Commit f0d35699 authored by Tomas Bulva's avatar Tomas Bulva

Header Search Refactor - Handle Errors in the component

Changed the way the api error is being handled on the front end for the header_search.
Moved page wide message about the error to inside the dropdown using GlAlert.

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