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 {
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 ""
......@@ -37200,9 +37203,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