Commit 0dd469c7 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '227349-package-registry-expand-the-filter-and-sort-functionality-on-the-package-registry-list-view' into 'master'

Redesign the search UI for the package list

See merge request gitlab-org/gitlab!52575
parents 2b2a7a48 d8d79922
<script> <script>
import { GlSorting, GlSortingItem } from '@gitlab/ui'; import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import PackageTypeToken from './tokens/package_type_token.vue';
import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants'; import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
import getTableHeaders from '../utils'; import getTableHeaders from '../utils';
export default { export default {
name: 'PackageSort',
components: { components: {
GlSorting, GlSorting,
GlSortingItem, GlSortingItem,
GlFilteredSearch,
}, },
computed: { computed: {
...mapState({ ...mapState({
isGroupPage: (state) => state.config.isGroupPage, isGroupPage: (state) => state.config.isGroupPage,
orderBy: (state) => state.sorting.orderBy, orderBy: (state) => state.sorting.orderBy,
sort: (state) => state.sorting.sort, sort: (state) => state.sorting.sort,
filter: (state) => state.filter,
}), }),
internalFilter: {
get() {
return this.filter;
},
set(value) {
this.setFilter(value);
},
},
sortText() { sortText() {
const field = this.sortableFields.find((s) => s.orderBy === this.orderBy); const field = this.sortableFields.find((s) => s.orderBy === this.orderBy);
return field ? field.label : ''; return field ? field.label : '';
...@@ -26,9 +37,21 @@ export default { ...@@ -26,9 +37,21 @@ export default {
isSortAscending() { isSortAscending() {
return this.sort === ASCENDING_ODER; return this.sort === ASCENDING_ODER;
}, },
tokens() {
return [
{
type: 'type',
icon: 'package',
title: s__('PackageRegistry|Type'),
unique: true,
token: PackageTypeToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
},
];
},
}, },
methods: { methods: {
...mapActions(['setSorting']), ...mapActions(['setSorting', 'setFilter']),
onDirectionChange() { onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER; const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort }); this.setSorting({ sort });
...@@ -38,11 +61,24 @@ export default { ...@@ -38,11 +61,24 @@ export default {
this.setSorting({ orderBy: item }); this.setSorting({ orderBy: item });
this.$emit('sort:changed'); this.$emit('sort:changed');
}, },
clearSearch() {
this.setFilter([]);
this.$emit('filter:changed');
},
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100">
<gl-filtered-search
v-model="internalFilter"
class="gl-mr-4 gl-flex-fill-1"
:placeholder="__('Filter results')"
:available-tokens="tokens"
@submit="$emit('filter:changed')"
@clear="clearSearch"
/>
<gl-sorting <gl-sorting
:text="sortText" :text="sortText"
:is-ascending="isSortAscending" :is-ascending="isSortAscending"
...@@ -57,4 +93,5 @@ export default { ...@@ -57,4 +93,5 @@ export default {
{{ item.label }} {{ item.label }}
</gl-sorting-item> </gl-sorting-item>
</gl-sorting> </gl-sorting>
</div>
</template> </template>
<script>
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mapActions } from 'vuex';
export default {
components: {
GlSearchBoxByClick,
},
methods: {
...mapActions(['setFilter']),
},
};
</script>
<template>
<gl-search-box-by-click
:placeholder="s__('PackageRegistry|Filter by name')"
@submit="$emit('filter')"
@input="setFilter"
/>
</template>
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui'; import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils'; import { historyReplaceState } from '~/lib/utils/common_utils';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue'; import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageTitle from './package_title.vue'; import PackageTitle from './package_title.vue';
import PackageSearch from './package_search.vue';
export default { export default {
components: { components: {
GlEmptyState, GlEmptyState,
GlTab,
GlTabs,
GlLink, GlLink,
GlSprintf, GlSprintf,
PackageFilter,
PackageList, PackageList,
PackageSort,
PackageTitle, PackageTitle,
PackageSearch,
}, },
computed: { computed: {
...mapState({ ...mapState({
emptyListIllustration: (state) => state.config.emptyListIllustration, emptyListIllustration: (state) => state.config.emptyListIllustration,
emptyListHelpUrl: (state) => state.config.emptyListHelpUrl, emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
filterQuery: (state) => state.filterQuery, filter: (state) => state.filter,
selectedType: (state) => state.selectedType, selectedType: (state) => state.selectedType,
packageHelpUrl: (state) => state.config.packageHelpUrl, packageHelpUrl: (state) => state.config.packageHelpUrl,
packagesCount: (state) => state.pagination?.total, packagesCount: (state) => state.pagination?.total,
}), }),
tabsToRender() { emptySearch() {
return PACKAGE_REGISTRY_TABS; return (
this.filter.filter((f) => f.type !== 'filtered-search-term' || f.value?.data).length === 0
);
},
emptyStateTitle() {
return this.emptySearch
? s__('PackageRegistry|There are no packages yet')
: s__('PackageRegistry|Sorry, your filter produced no results');
}, },
}, },
mounted() { mounted() {
...@@ -48,27 +52,6 @@ export default { ...@@ -48,27 +52,6 @@ export default {
onPackageDeleteRequest(item) { onPackageDeleteRequest(item) {
return this.requestDeletePackage(item); return this.requestDeletePackage(item);
}, },
tabChanged(index) {
const selected = PACKAGE_REGISTRY_TABS[index];
if (selected !== this.selectedType) {
this.setSelectedType(selected);
this.requestPackagesList();
}
},
emptyStateTitle({ title, type }) {
if (this.filterQuery) {
return s__('PackageRegistry|Sorry, your filter produced no results');
}
if (type) {
return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
packageType: title,
});
}
return s__('PackageRegistry|There are no packages yet');
},
checkDeleteAlert() { checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT); const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
...@@ -92,23 +75,13 @@ export default { ...@@ -92,23 +75,13 @@ export default {
<template> <template>
<div> <div>
<package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" /> <package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" />
<package-search @sort:changed="requestPackagesList" @filter:changed="requestPackagesList" />
<gl-tabs @input="tabChanged">
<template #tabs-end>
<div
class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
>
<package-filter class="gl-mr-2" @filter="requestPackagesList" />
<package-sort @sort:changed="requestPackagesList" />
</div>
</template>
<gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state> <template #empty-state>
<gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration"> <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
<template #description> <template #description>
<gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" /> <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
<gl-sprintf v-else :message="$options.i18n.noResults"> <gl-sprintf v-else :message="$options.i18n.noResults">
<template #noPackagesLink="{ content }"> <template #noPackagesLink="{ content }">
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
...@@ -118,7 +91,5 @@ export default { ...@@ -118,7 +91,5 @@ export default {
</gl-empty-state> </gl-empty-state>
</template> </template>
</package-list> </package-list>
</gl-tab>
</gl-tabs>
</div> </div>
</template> </template>
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { PACKAGE_TYPES } from '../../constants';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
},
PACKAGE_TYPES,
};
</script>
<template>
<gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
<template #suggestions>
<gl-filtered-search-suggestion
v-for="(type, index) in $options.PACKAGE_TYPES"
:key="index"
:value="type.type"
>
{{ type.title }}
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
</template>
...@@ -55,11 +55,7 @@ export const SORT_FIELDS = [ ...@@ -55,11 +55,7 @@ export const SORT_FIELDS = [
}, },
]; ];
export const PACKAGE_REGISTRY_TABS = [ export const PACKAGE_TYPES = [
{
title: __('All'),
type: null,
},
{ {
title: s__('PackageRegistry|Composer'), title: s__('PackageRegistry|Composer'),
type: PackageType.COMPOSER, type: PackageType.COMPOSER,
......
...@@ -15,7 +15,6 @@ import { getNewPaginationPage } from '../utils'; ...@@ -15,7 +15,6 @@ import { getNewPaginationPage } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data); export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data); export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data); export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
...@@ -29,9 +28,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => { ...@@ -29,9 +28,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params; const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting; const { sort, orderBy } = state.sorting;
const type = state.selectedType?.type?.toLowerCase(); const type = state.filter.find((f) => f.type === 'type');
const nameFilter = state.filterQuery?.toLowerCase(); const name = state.filter.find((f) => f.type === 'filtered-search-term');
const packageFilters = { package_type: type, package_name: nameFilter }; const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages'; const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
......
...@@ -4,5 +4,4 @@ export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS'; ...@@ -4,5 +4,4 @@ export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION'; export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING'; export const SET_SORTING = 'SET_SORTING';
export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
export const SET_FILTER = 'SET_FILTER'; export const SET_FILTER = 'SET_FILTER';
...@@ -28,11 +28,7 @@ export default { ...@@ -28,11 +28,7 @@ export default {
state.sorting = { ...state.sorting, ...sorting }; state.sorting = { ...state.sorting, ...sorting };
}, },
[types.SET_SELECTED_TYPE](state, type) { [types.SET_FILTER](state, filter) {
state.selectedType = type; state.filter = filter;
},
[types.SET_FILTER](state, query) {
state.filterQuery = query;
}, },
}; };
import { PACKAGE_REGISTRY_TABS } from '../constants';
export default () => ({ export default () => ({
/** /**
* Determine if the component is loading data from the API * Determine if the component is loading data from the API
...@@ -49,9 +47,8 @@ export default () => ({ ...@@ -49,9 +47,8 @@ export default () => ({
/** /**
* The search query that is used to filter packages by name * The search query that is used to filter packages by name
*/ */
filterQuery: '', filter: [],
/** /**
* The selected TAB of the package types tabs * The selected TAB of the package types tabs
*/ */
selectedType: PACKAGE_REGISTRY_TABS[0],
}); });
---
title: Redesign the search UI for the package list
merge_request: 52575
author:
type: changed
...@@ -20804,9 +20804,6 @@ msgstr "" ...@@ -20804,9 +20804,6 @@ msgstr ""
msgid "PackageRegistry|Delete package" msgid "PackageRegistry|Delete package"
msgstr "" msgstr ""
msgid "PackageRegistry|Filter by name"
msgstr ""
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}" msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
msgstr "" msgstr ""
...@@ -20900,9 +20897,6 @@ msgstr "" ...@@ -20900,9 +20897,6 @@ msgstr ""
msgid "PackageRegistry|Source project located at %{link}" msgid "PackageRegistry|Source project located at %{link}"
msgstr "" msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
msgid "PackageRegistry|There are no other versions of this package." msgid "PackageRegistry|There are no other versions of this package."
msgstr "" msgstr ""
...@@ -20918,6 +20912,9 @@ msgstr "" ...@@ -20918,6 +20912,9 @@ msgstr ""
msgid "PackageRegistry|To widen your search, change or remove the filters above." msgid "PackageRegistry|To widen your search, change or remove the filters above."
msgstr "" msgstr ""
msgid "PackageRegistry|Type"
msgstr ""
msgid "PackageRegistry|Unable to fetch package version information." msgid "PackageRegistry|Unable to fetch package version information."
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_filter renders 1`] = `
<gl-search-box-by-click-stub
clearable="true"
clearbuttontitle="Clear"
clearrecentsearchestext="Clear recent searches"
closebuttontitle="Close"
norecentsearchestext="You don't have any recent searches"
placeholder="Filter by name"
recentsearchesheader="Recent searches"
value=""
/>
`;
import Vuex from 'vuex';
import { GlSearchBoxByClick } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import PackagesFilter from '~/packages/list/components/packages_filter.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_filter', () => {
let wrapper;
let store;
const findGlSearchBox = () => wrapper.find(GlSearchBoxByClick);
const mountComponent = () => {
store = new Vuex.Store();
store.dispatch = jest.fn();
wrapper = shallowMount(PackagesFilter, {
localVue,
store,
});
};
beforeEach(mountComponent);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('emits events', () => {
it('sets the filter value in the store on input', () => {
const searchString = 'foo';
findGlSearchBox().vm.$emit('input', searchString);
expect(store.dispatch).toHaveBeenCalledWith('setFilter', searchString);
});
it('emits the filter event when search box is submitted', () => {
findGlSearchBox().vm.$emit('submit');
expect(wrapper.emitted('filter')).toBeTruthy();
});
});
});
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui'; import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import PackageListApp from '~/packages/list/components/packages_list_app.vue'; import PackageListApp from '~/packages/list/components/packages_list_app.vue';
import PackageSearch from '~/packages/list/components/package_search.vue';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
...@@ -26,9 +27,9 @@ describe('packages_list_app', () => { ...@@ -26,9 +27,9 @@ describe('packages_list_app', () => {
const emptyListHelpUrl = 'helpUrl'; const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList); const findListComponent = () => wrapper.find(PackageList);
const findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index); const findPackageSearch = () => wrapper.find(PackageSearch);
const createStore = (filterQuery = '') => { const createStore = (filter = []) => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
isLoading: false, isLoading: false,
...@@ -38,7 +39,7 @@ describe('packages_list_app', () => { ...@@ -38,7 +39,7 @@ describe('packages_list_app', () => {
emptyListHelpUrl, emptyListHelpUrl,
packageHelpUrl: 'foo', packageHelpUrl: 'foo',
}, },
filterQuery, filter,
}, },
}); });
store.dispatch = jest.fn(); store.dispatch = jest.fn();
...@@ -52,8 +53,6 @@ describe('packages_list_app', () => { ...@@ -52,8 +53,6 @@ describe('packages_list_app', () => {
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
PackageList, PackageList,
GlTab,
GlTabs,
GlSprintf, GlSprintf,
GlLink, GlLink,
}, },
...@@ -122,27 +121,9 @@ describe('packages_list_app', () => { ...@@ -122,27 +121,9 @@ describe('packages_list_app', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledTimes(1);
}); });
describe('tab change', () => {
it('calls requestPackagesList when all tab is clicked', () => {
mountComponent();
findTabComponent().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
it('calls requestPackagesList when a package type tab is clicked', () => {
mountComponent();
findTabComponent(1).trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
describe('filter without results', () => { describe('filter without results', () => {
beforeEach(() => { beforeEach(() => {
createStore('foo'); createStore([{ type: 'something' }]);
mountComponent(); mountComponent();
}); });
...@@ -154,12 +135,28 @@ describe('packages_list_app', () => { ...@@ -154,12 +135,28 @@ describe('packages_list_app', () => {
}); });
}); });
describe('Package Search', () => {
it('exists', () => {
mountComponent();
expect(findPackageSearch().exists()).toBe(true);
});
it.each(['sort:changed', 'filter:changed'])('on %p fetches data from the store', (event) => {
mountComponent();
findPackageSearch().vm.$emit(event);
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
describe('delete alert handling', () => { describe('delete alert handling', () => {
const { location } = window.location; const { location } = window.location;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`; const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => { beforeEach(() => {
createStore('foo'); createStore();
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {}); jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
delete window.location; delete window.location;
window.location = { window.location = {
......
import Vuex from 'vuex';
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import component from '~/packages/list/components/package_search.vue';
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Package Search', () => {
let wrapper;
let store;
let sorting;
let sortingItems;
const findPackageListSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const createStore = (isGroupPage) => {
const state = {
config: {
isGroupPage,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
filter: [],
};
store = new Vuex.Store({
state,
});
store.dispatch = jest.fn();
};
const mountComponent = (isGroupPage = false) => {
createStore(isGroupPage);
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlSortingItem,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('searching', () => {
it('has a filtered-search component', () => {
mountComponent();
expect(findFilteredSearch().exists()).toBe(true);
});
it('binds the correct props to filtered-search', () => {
mountComponent();
expect(findFilteredSearch().props()).toMatchObject({
value: [],
placeholder: 'Filter results',
availableTokens: wrapper.vm.tokens,
});
});
it('updates vuex when value changes', () => {
mountComponent();
findFilteredSearch().vm.$emit('input', ['foo']);
expect(store.dispatch).toHaveBeenCalledWith('setFilter', ['foo']);
});
it('emits filter:changed on submit event', () => {
mountComponent();
findFilteredSearch().vm.$emit('submit');
expect(wrapper.emitted('filter:changed')).toEqual([[]]);
});
it('emits filter:changed on clear event and reset vuex', () => {
mountComponent();
findFilteredSearch().vm.$emit('clear');
expect(store.dispatch).toHaveBeenCalledWith('setFilter', []);
expect(wrapper.emitted('filter:changed')).toEqual([[]]);
});
it('has a PackageTypeToken token', () => {
mountComponent();
expect(findFilteredSearch().props('availableTokens')).toEqual(
expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
);
});
});
describe('sorting', () => {
describe('when is in projects', () => {
beforeEach(() => {
mountComponent();
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
});
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
it('on sort item click set sorting and emit event', () => {
const item = sortingItems.at(0);
const { orderBy } = wrapper.vm.sortableFields[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
describe('when is in group', () => {
beforeEach(() => {
mountComponent(true);
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
});
});
});
});
import Vuex from 'vuex';
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import PackagesSort from '~/packages/list/components/packages_sort.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_sort', () => {
let wrapper;
let store;
let sorting;
let sortingItems;
const findPackageListSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
const createStore = (isGroupPage) => {
const state = {
config: {
isGroupPage,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
};
store = new Vuex.Store({
state,
});
store.dispatch = jest.fn();
};
const mountComponent = (isGroupPage = false) => {
createStore(isGroupPage);
wrapper = mount(PackagesSort, {
localVue,
store,
stubs: {
...stubChildren(PackagesSort),
GlSortingItem,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when is in projects', () => {
beforeEach(() => {
mountComponent();
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
});
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
it('on sort item click set sorting and emit event', () => {
const item = sortingItems.at(0);
const { orderBy } = wrapper.vm.sortableFields[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
describe('when is in group', () => {
beforeEach(() => {
mountComponent(true);
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import component from '~/packages/list/components/tokens/package_type_token.vue';
import { PACKAGE_TYPES } from '~/packages/list/constants';
describe('packages_filter', () => {
let wrapper;
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const mountComponent = ({ attrs, listeners } = {}) => {
wrapper = shallowMount(component, {
attrs,
listeners,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('it binds all of his attrs to filtered search token', () => {
mountComponent({ attrs: { foo: 'bar' } });
expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
});
it('it binds all of his events to filtered search token', () => {
const clickListener = jest.fn();
mountComponent({ listeners: { click: clickListener } });
findFilteredSearchToken().vm.$emit('click');
expect(clickListener).toHaveBeenCalled();
});
it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
'displays a suggestion for %p',
(packageType, index) => {
mountComponent();
const item = findFilteredSearchSuggestions().at(index);
expect(item.text()).toBe(packageType.title);
expect(item.props('value')).toBe(packageType.type);
},
);
});
...@@ -30,11 +30,13 @@ describe('Actions Package list store', () => { ...@@ -30,11 +30,13 @@ describe('Actions Package list store', () => {
sort: 'asc', sort: 'asc',
orderBy: 'version', orderBy: 'version',
}; };
const filter = [];
it('should fetch the project packages list when isGroupPage is false', (done) => { it('should fetch the project packages list when isGroupPage is false', (done) => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: false, resourceId: 1 }, sorting }, { config: { isGroupPage: false, resourceId: 1 }, sorting, filter },
[], [],
[ [
{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: true },
...@@ -54,7 +56,7 @@ describe('Actions Package list store', () => { ...@@ -54,7 +56,7 @@ describe('Actions Package list store', () => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: true, resourceId: 2 }, sorting }, { config: { isGroupPage: true, resourceId: 2 }, sorting, filter },
[], [],
[ [
{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: true },
...@@ -70,7 +72,7 @@ describe('Actions Package list store', () => { ...@@ -70,7 +72,7 @@ describe('Actions Package list store', () => {
); );
}); });
it('should fetch packages of a certain type when selectedType is present', (done) => { it('should fetch packages of a certain type when a filter with a type is present', (done) => {
const packageType = 'maven'; const packageType = 'maven';
testAction( testAction(
...@@ -79,7 +81,7 @@ describe('Actions Package list store', () => { ...@@ -79,7 +81,7 @@ describe('Actions Package list store', () => {
{ {
config: { isGroupPage: false, resourceId: 1 }, config: { isGroupPage: false, resourceId: 1 },
sorting, sorting,
selectedType: { type: packageType }, filter: [{ type: 'type', value: { data: 'maven' } }],
}, },
[], [],
[ [
...@@ -107,7 +109,7 @@ describe('Actions Package list store', () => { ...@@ -107,7 +109,7 @@ describe('Actions Package list store', () => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: false, resourceId: 2 }, sorting }, { config: { isGroupPage: false, resourceId: 2 }, sorting, filter },
[], [],
[ [
{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: true },
......
...@@ -78,17 +78,10 @@ describe('Mutations Registry Store', () => { ...@@ -78,17 +78,10 @@ describe('Mutations Registry Store', () => {
}); });
}); });
describe('SET_SELECTED_TYPE', () => {
it('should set the selected type', () => {
mutations[types.SET_SELECTED_TYPE](mockState, { type: 'maven' });
expect(mockState.selectedType).toEqual({ type: 'maven' });
});
});
describe('SET_FILTER', () => { describe('SET_FILTER', () => {
it('should set the filter query', () => { it('should set the filter query', () => {
mutations[types.SET_FILTER](mockState, 'foo'); mutations[types.SET_FILTER](mockState, 'foo');
expect(mockState.filterQuery).toEqual('foo'); expect(mockState.filter).toEqual('foo');
}); });
}); });
}); });
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