Commit 896a0966 authored by Alex Pooley's avatar Alex Pooley

Merge branch '341287' into 'master'

Header Search Refactor - Handle Errors in the component

See merge request gitlab-org/gitlab!80746
parents 7cad36d0 f0d35699
...@@ -4,20 +4,28 @@ import { ...@@ -4,20 +4,28 @@ import {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownDivider, GlDropdownDivider,
GlAvatar, GlAvatar,
GlAlert,
GlLoadingIcon, GlLoadingIcon,
GlSafeHtmlDirective as SafeHtml, GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import highlight from '~/lib/utils/highlight'; import highlight from '~/lib/utils/highlight';
import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants'; import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
export default { export default {
name: 'HeaderSearchAutocompleteItems', name: 'HeaderSearchAutocompleteItems',
i18n: {
autocompleteErrorMessage: s__(
'GlobalSearch|There was an error fetching search autocomplete suggestions.',
),
},
components: { components: {
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownDivider, GlDropdownDivider,
GlAvatar, GlAvatar,
GlAlert,
GlLoadingIcon, GlLoadingIcon,
}, },
directives: { directives: {
...@@ -31,7 +39,7 @@ export default { ...@@ -31,7 +39,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['search', 'loading']), ...mapState(['search', 'loading', 'autocompleteError']),
...mapGetters(['autocompleteGroupedSearchOptions']), ...mapGetters(['autocompleteGroupedSearchOptions']),
}, },
watch: { watch: {
...@@ -93,5 +101,13 @@ export default { ...@@ -93,5 +101,13 @@ export default {
</div> </div>
</template> </template>
<gl-loading-icon v-else size="lg" class="my-4" /> <gl-loading-icon v-else size="lg" class="my-4" />
<gl-alert
v-if="autocompleteError"
class="gl-text-body gl-mt-2"
:dismissible="false"
variant="danger"
>
{{ $options.i18n.autocompleteErrorMessage }}
</gl-alert>
</div> </div>
</template> </template>
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const fetchAutocompleteOptions = ({ commit, getters }) => { export const fetchAutocompleteOptions = ({ commit, getters }) => {
...@@ -10,7 +8,6 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => { ...@@ -10,7 +8,6 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => {
.then(({ data }) => commit(types.RECEIVE_AUTOCOMPLETE_SUCCESS, data)) .then(({ data }) => commit(types.RECEIVE_AUTOCOMPLETE_SUCCESS, data))
.catch(() => { .catch(() => {
commit(types.RECEIVE_AUTOCOMPLETE_ERROR); commit(types.RECEIVE_AUTOCOMPLETE_ERROR);
createFlash({ message: __('There was an error fetching search autocomplete suggestions') });
}); });
}; };
......
...@@ -4,19 +4,23 @@ export default { ...@@ -4,19 +4,23 @@ export default {
[types.REQUEST_AUTOCOMPLETE](state) { [types.REQUEST_AUTOCOMPLETE](state) {
state.loading = true; state.loading = true;
state.autocompleteOptions = []; state.autocompleteOptions = [];
state.autocompleteError = false;
}, },
[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) { [types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
state.loading = false; state.loading = false;
state.autocompleteOptions = data.map((d, i) => { state.autocompleteOptions = data.map((d, i) => {
return { html_id: `autocomplete-${d.category}-${i}`, ...d }; return { html_id: `autocomplete-${d.category}-${i}`, ...d };
}); });
state.autocompleteError = false;
}, },
[types.RECEIVE_AUTOCOMPLETE_ERROR](state) { [types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
state.loading = false; state.loading = false;
state.autocompleteOptions = []; state.autocompleteOptions = [];
state.autocompleteError = true;
}, },
[types.CLEAR_AUTOCOMPLETE](state) { [types.CLEAR_AUTOCOMPLETE](state) {
state.autocompleteOptions = []; state.autocompleteOptions = [];
state.autocompleteError = false;
}, },
[types.SET_SEARCH](state, value) { [types.SET_SEARCH](state, value) {
state.search = value; state.search = value;
......
...@@ -6,6 +6,7 @@ const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchC ...@@ -6,6 +6,7 @@ const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchC
searchContext, searchContext,
search: '', search: '',
autocompleteOptions: [], autocompleteOptions: [],
autocompleteError: false,
loading: false, loading: false,
}); });
export default createState; export default createState;
...@@ -1810,6 +1810,9 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav { ...@@ -1810,6 +1810,9 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav {
body.gl-dark .navbar-gitlab .nav > li { body.gl-dark .navbar-gitlab .nav > li {
color: #fafafa; color: #fafafa;
} }
body.gl-dark .navbar-gitlab .nav > li.header-search-new {
color: #fafafa;
}
body.gl-dark .navbar-gitlab .nav > li > a .notification-dot { body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
border: 2px solid #fafafa; border: 2px solid #fafafa;
} }
...@@ -1847,8 +1850,8 @@ body.gl-dark ...@@ -1847,8 +1850,8 @@ body.gl-dark
body.gl-dark .header-search { body.gl-dark .header-search {
background-color: rgba(250, 250, 250, 0.2) !important; background-color: rgba(250, 250, 250, 0.2) !important;
} }
body.gl-dark .header-search svg { body.gl-dark .header-search svg.gl-search-box-by-type-search-icon {
color: rgba(250, 250, 250, 0.8) !important; color: rgba(250, 250, 250, 0.8);
} }
body.gl-dark .header-search input { body.gl-dark .header-search input {
background-color: transparent; background-color: transparent;
......
...@@ -64,6 +64,10 @@ ...@@ -64,6 +64,10 @@
> li { > li {
color: $search-and-nav-links; color: $search-and-nav-links;
&.header-search-new {
color: $sidebar-text;
}
> a { > a {
.notification-dot { .notification-dot {
border: 2px solid $nav-svg-color; border: 2px solid $nav-svg-color;
...@@ -151,10 +155,11 @@ ...@@ -151,10 +155,11 @@
background-color: rgba($search-and-nav-links, 0.3) !important; background-color: rgba($search-and-nav-links, 0.3) !important;
} }
svg { svg.gl-search-box-by-type-search-icon {
color: rgba($search-and-nav-links, 0.8) !important; color: rgba($search-and-nav-links, 0.8);
} }
input { input {
background-color: transparent; background-color: transparent;
color: rgba($search-and-nav-links, 0.8); color: rgba($search-and-nav-links, 0.8);
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
= render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block' = render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block'
- if top_nav_show_search - if top_nav_show_search
- search_menu_item = top_nav_search_menu_item_attrs - search_menu_item = top_nav_search_menu_item_attrs
%li.nav-item.d-none.d-lg-block.m-auto %li.nav-item.header-search-new.d-none.d-lg-block.m-auto
- unless current_controller?(:search) - unless current_controller?(:search)
- if Feature.enabled?(:new_header_search) - if Feature.enabled?(:new_header_search)
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json, #js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
......
...@@ -1810,6 +1810,9 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav { ...@@ -1810,6 +1810,9 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav {
body.gl-dark .navbar-gitlab .nav > li { body.gl-dark .navbar-gitlab .nav > li {
color: #fafafa; color: #fafafa;
} }
body.gl-dark .navbar-gitlab .nav > li.header-search-new {
color: #fafafa;
}
body.gl-dark .navbar-gitlab .nav > li > a .notification-dot { body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
border: 2px solid #fafafa; border: 2px solid #fafafa;
} }
...@@ -1847,8 +1850,8 @@ body.gl-dark ...@@ -1847,8 +1850,8 @@ body.gl-dark
body.gl-dark .header-search { body.gl-dark .header-search {
background-color: rgba(250, 250, 250, 0.2) !important; background-color: rgba(250, 250, 250, 0.2) !important;
} }
body.gl-dark .header-search svg { body.gl-dark .header-search svg.gl-search-box-by-type-search-icon {
color: rgba(250, 250, 250, 0.8) !important; color: rgba(250, 250, 250, 0.8);
} }
body.gl-dark .header-search input { body.gl-dark .header-search input {
background-color: transparent; background-color: transparent;
......
...@@ -16837,6 +16837,9 @@ msgstr "" ...@@ -16837,6 +16837,9 @@ msgstr ""
msgid "GlobalSearch|Search results are loading" msgid "GlobalSearch|Search results are loading"
msgstr "" msgstr ""
msgid "GlobalSearch|There was an error fetching search autocomplete suggestions."
msgstr ""
msgid "GlobalSearch|Type and press the enter key to submit search." msgid "GlobalSearch|Type and press the enter key to submit search."
msgstr "" msgstr ""
...@@ -37200,9 +37203,6 @@ msgstr "" ...@@ -37200,9 +37203,6 @@ msgstr ""
msgid "There was an error fetching projects" msgid "There was an error fetching projects"
msgstr "" msgstr ""
msgid "There was an error fetching search autocomplete suggestions"
msgstr ""
msgid "There was an error fetching stage total counts" msgid "There was an error fetching stage total counts"
msgstr "" msgstr ""
......
import { GlDropdownItem, GlLoadingIcon, GlAvatar } from '@gitlab/ui'; import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -46,6 +46,7 @@ describe('HeaderSearchAutocompleteItems', () => { ...@@ -46,6 +46,7 @@ describe('HeaderSearchAutocompleteItems', () => {
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGlAvatar = () => wrapper.findComponent(GlAvatar); const findGlAvatar = () => wrapper.findComponent(GlAvatar);
const findGlAlert = () => wrapper.findComponent(GlAlert);
describe('template', () => { describe('template', () => {
describe('when loading is true', () => { describe('when loading is true', () => {
...@@ -62,6 +63,15 @@ describe('HeaderSearchAutocompleteItems', () => { ...@@ -62,6 +63,15 @@ describe('HeaderSearchAutocompleteItems', () => {
}); });
}); });
describe('when api returns error', () => {
beforeEach(() => {
createComponent({ autocompleteError: true });
});
it('renders Alert', () => {
expect(findGlAlert().exists()).toBe(true);
});
});
describe('when loading is false', () => { describe('when loading is false', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ loading: false }); createComponent({ loading: false });
...@@ -86,6 +96,7 @@ describe('HeaderSearchAutocompleteItems', () => { ...@@ -86,6 +96,7 @@ describe('HeaderSearchAutocompleteItems', () => {
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
}); });
}); });
describe.each` describe.each`
item | showAvatar | avatarSize item | showAvatar | avatarSize
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import * as actions from '~/header_search/store/actions'; import * as actions from '~/header_search/store/actions';
import * as types from '~/header_search/store/mutation_types'; import * as types from '~/header_search/store/mutation_types';
import createState from '~/header_search/store/state'; import createState from '~/header_search/store/state';
...@@ -13,11 +12,6 @@ describe('Header Search Store Actions', () => { ...@@ -13,11 +12,6 @@ describe('Header Search Store Actions', () => {
let state; let state;
let mock; let mock;
const flashCallback = (callCount) => {
expect(createFlash).toHaveBeenCalledTimes(callCount);
createFlash.mockClear();
};
beforeEach(() => { beforeEach(() => {
state = createState({}); state = createState({});
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -29,10 +23,10 @@ describe('Header Search Store Actions', () => { ...@@ -29,10 +23,10 @@ describe('Header Search Store Actions', () => {
}); });
describe.each` describe.each`
axiosMock | type | expectedMutations | flashCallCount axiosMock | type | expectedMutations
${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]} | ${0} ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} | ${1} ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations, flashCallCount }) => { `('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
describe(`on ${type}`, () => { describe(`on ${type}`, () => {
beforeEach(() => { beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res); mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
...@@ -42,7 +36,7 @@ describe('Header Search Store Actions', () => { ...@@ -42,7 +36,7 @@ describe('Header Search Store Actions', () => {
action: actions.fetchAutocompleteOptions, action: actions.fetchAutocompleteOptions,
state, state,
expectedMutations, expectedMutations,
}).then(() => flashCallback(flashCallCount)); });
}); });
}); });
}); });
......
...@@ -20,6 +20,7 @@ describe('Header Search Store Mutations', () => { ...@@ -20,6 +20,7 @@ describe('Header Search Store Mutations', () => {
expect(state.loading).toBe(true); expect(state.loading).toBe(true);
expect(state.autocompleteOptions).toStrictEqual([]); expect(state.autocompleteOptions).toStrictEqual([]);
expect(state.autocompleteError).toBe(false);
}); });
}); });
...@@ -29,6 +30,7 @@ describe('Header Search Store Mutations', () => { ...@@ -29,6 +30,7 @@ describe('Header Search Store Mutations', () => {
expect(state.loading).toBe(false); expect(state.loading).toBe(false);
expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS); expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
expect(state.autocompleteError).toBe(false);
}); });
}); });
...@@ -38,6 +40,7 @@ describe('Header Search Store Mutations', () => { ...@@ -38,6 +40,7 @@ describe('Header Search Store Mutations', () => {
expect(state.loading).toBe(false); expect(state.loading).toBe(false);
expect(state.autocompleteOptions).toStrictEqual([]); expect(state.autocompleteOptions).toStrictEqual([]);
expect(state.autocompleteError).toBe(true);
}); });
}); });
...@@ -46,6 +49,7 @@ describe('Header Search Store Mutations', () => { ...@@ -46,6 +49,7 @@ describe('Header Search Store Mutations', () => {
mutations[types.CLEAR_AUTOCOMPLETE](state); mutations[types.CLEAR_AUTOCOMPLETE](state);
expect(state.autocompleteOptions).toStrictEqual([]); expect(state.autocompleteOptions).toStrictEqual([]);
expect(state.autocompleteError).toBe(false);
}); });
}); });
......
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