Commit a6f2c19c authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '197920-add-filter-by-name-option-to-the-package-list-view-user-interface' into 'master'

Add name filter option to packages list

See merge request gitlab-org/gitlab!27586
parents db0673b1 95839318
---
title: Adds filter by name to the packages list
merge_request: 27586
author:
type: added
<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>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs } from '@gitlab/ui'; import { GlEmptyState, GlTab, GlTabs } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
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 PackageSort from './packages_sort.vue';
import { PACKAGE_REGISTRY_TABS } from '../constants'; import { PACKAGE_REGISTRY_TABS } from '../constants';
...@@ -11,17 +12,21 @@ export default { ...@@ -11,17 +12,21 @@ export default {
GlEmptyState, GlEmptyState,
GlTab, GlTab,
GlTabs, GlTabs,
PackageFilter,
PackageList, PackageList,
PackageSort, PackageSort,
}, },
computed: { computed: {
...mapState({ ...mapState({
resourceId: state => state.config.resourceId,
emptyListIllustration: state => state.config.emptyListIllustration, emptyListIllustration: state => state.config.emptyListIllustration,
emptyListHelpUrl: state => state.config.emptyListHelpUrl, emptyListHelpUrl: state => state.config.emptyListHelpUrl,
totalItems: state => state.pagination.total, filterQuery: state => state.filterQuery,
}), }),
emptyListText() { emptyListText() {
if (this.filterQuery) {
return s__('PackageRegistry|To widen your search, change or remove the filters above.');
}
return sprintf( return sprintf(
s__( s__(
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
...@@ -55,6 +60,10 @@ export default { ...@@ -55,6 +60,10 @@ export default {
this.requestPackagesList(); this.requestPackagesList();
}, },
emptyStateTitle({ title, type }) { emptyStateTitle({ title, type }) {
if (this.filterQuery) {
return s__('PackageRegistry|Sorry, your filter produced no results');
}
if (type) { if (type) {
return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), { return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
packageType: title, packageType: title,
...@@ -70,7 +79,8 @@ export default { ...@@ -70,7 +79,8 @@ export default {
<template> <template>
<gl-tabs @input="tabChanged"> <gl-tabs @input="tabChanged">
<template #tabs-end> <template #tabs-end>
<div class="align-self-center ml-auto"> <div class="d-flex align-self-center ml-md-auto py-1 py-md-0">
<package-filter class="mr-1" @filter="requestPackagesList" />
<package-sort @sort:changed="requestPackagesList" /> <package-sort @sort:changed="requestPackagesList" />
</div> </div>
</template> </template>
......
...@@ -15,6 +15,7 @@ export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_ST ...@@ -15,6 +15,7 @@ export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_ST
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 setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_PACKAGE_LIST_SUCCESS, data); commit(types.SET_PACKAGE_LIST_SUCCESS, data);
...@@ -26,12 +27,15 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => { ...@@ -26,12 +27,15 @@ 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.selectedType?.type?.toLowerCase();
const packageType = { package_type: type }; const nameFilter = state.filterQuery?.toLowerCase();
const packageFilters = { package_type: type, package_name: nameFilter };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages'; const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
return Api[apiMethod](state.config.resourceId, { return Api[apiMethod](state.config.resourceId, {
params: { page, per_page, sort, order_by: orderBy, ...packageType }, params: { page, per_page, sort, order_by: orderBy, ...packageFilters },
}) })
.then(({ data, headers }) => { .then(({ data, headers }) => {
dispatch('receivePackagesListSuccess', { data, headers }); dispatch('receivePackagesListSuccess', { data, headers });
......
...@@ -5,3 +5,4 @@ export const SET_PAGINATION = 'SET_PAGINATION'; ...@@ -5,3 +5,4 @@ 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_SELECTED_TYPE = 'SET_SELECTED_TYPE';
export const SET_FILTER = 'SET_FILTER';
...@@ -30,4 +30,8 @@ export default { ...@@ -30,4 +30,8 @@ export default {
[types.SET_SELECTED_TYPE](state, type) { [types.SET_SELECTED_TYPE](state, type) {
state.selectedType = type; state.selectedType = type;
}, },
[types.SET_FILTER](state, query) {
state.filterQuery = query;
},
}; };
...@@ -43,4 +43,8 @@ export default () => ({ ...@@ -43,4 +43,8 @@ export default () => ({
sort: 'desc', sort: 'desc',
orderBy: 'created_at', orderBy: 'created_at',
}, },
/**
* The search query that is used to filter packages by name
*/
filterQuery: '',
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_filter renders 1`] = `
<gl-search-box-by-click-stub
clearbuttontitle="Clear"
clearrecentsearchestext="Clear recent searches"
closebuttontitle="Close"
norecentsearchestext="You don't have any recent searches"
placeholder="Filter by name"
recentsearchesheader="Recent searches"
value=""
/>
`;
...@@ -350,8 +350,12 @@ exports[`packages_list_app renders 1`] = ` ...@@ -350,8 +350,12 @@ exports[`packages_list_app renders 1`] = `
</template> </template>
<template> <template>
<div <div
class="align-self-center ml-auto" class="d-flex align-self-center ml-md-auto py-1 py-md-0"
> >
<package-filter-stub
class="mr-1"
/>
<package-sort-stub /> <package-sort-stub />
</div> </div>
</template> </template>
......
import Vuex from 'vuex';
import { GlSearchBoxByClick } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import PackagesFilter from 'ee/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();
});
});
});
...@@ -17,9 +17,25 @@ describe('packages_list_app', () => { ...@@ -17,9 +17,25 @@ describe('packages_list_app', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' }; const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const emptyListHelpUrl = 'helpUrl'; const emptyListHelpUrl = 'helpUrl';
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 findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index);
const createStore = (filterQuery = '') => {
store = new Vuex.Store({
state: {
isLoading: false,
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
},
filterQuery,
},
});
store.dispatch = jest.fn();
};
const mountComponent = () => { const mountComponent = () => {
wrapper = shallowMount(PackageListApp, { wrapper = shallowMount(PackageListApp, {
localVue, localVue,
...@@ -35,17 +51,7 @@ describe('packages_list_app', () => { ...@@ -35,17 +51,7 @@ describe('packages_list_app', () => {
}; };
beforeEach(() => { beforeEach(() => {
store = new Vuex.Store({ createStore();
state: {
isLoading: false,
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
},
},
});
store.dispatch = jest.fn();
mountComponent(); mountComponent();
}); });
...@@ -73,7 +79,7 @@ describe('packages_list_app', () => { ...@@ -73,7 +79,7 @@ describe('packages_list_app', () => {
}); });
it('includes the right content on the default tab', () => { it('includes the right content on the default tab', () => {
const heading = findListComponent().find('h4'); const heading = findEmptyState().find('h4');
expect(heading.text()).toBe('There are no packages yet'); expect(heading.text()).toBe('There are no packages yet');
}); });
...@@ -110,4 +116,18 @@ describe('packages_list_app', () => { ...@@ -110,4 +116,18 @@ describe('packages_list_app', () => {
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList'); expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
}); });
}); });
describe('filter without results', () => {
beforeEach(() => {
createStore('foo');
mountComponent();
});
it('should show specific empty message', () => {
expect(findEmptyState().text()).toContain('Sorry, your filter produced no results');
expect(findEmptyState().text()).toContain(
'To widen your search, change or remove the filters above',
);
});
});
}); });
...@@ -228,4 +228,17 @@ describe('Actions Package list store', () => { ...@@ -228,4 +228,17 @@ describe('Actions Package list store', () => {
); );
}); });
}); });
describe('setFilter', () => {
it('should commit SET_FILTER', done => {
testAction(
actions.setFilter,
'foo',
null,
[{ type: types.SET_FILTER, payload: 'foo' }],
[],
done,
);
});
});
}); });
...@@ -84,4 +84,11 @@ describe('Mutations Registry Store', () => { ...@@ -84,4 +84,11 @@ describe('Mutations Registry Store', () => {
expect(mockState.selectedType).toEqual({ type: 'maven' }); expect(mockState.selectedType).toEqual({ type: 'maven' });
}); });
}); });
describe('SET_FILTER', () => {
it('should set the filter query', () => {
mutations[types.SET_FILTER](mockState, 'foo');
expect(mockState.filterQuery).toEqual('foo');
});
});
}); });
...@@ -14087,6 +14087,9 @@ msgstr "" ...@@ -14087,6 +14087,9 @@ msgstr ""
msgid "PackageRegistry|Delete package" msgid "PackageRegistry|Delete package"
msgstr "" msgstr ""
msgid "PackageRegistry|Filter by name"
msgstr ""
msgid "PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}." msgid "PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
msgstr "" msgstr ""
...@@ -14135,6 +14138,9 @@ msgstr "" ...@@ -14135,6 +14138,9 @@ msgstr ""
msgid "PackageRegistry|Remove package" msgid "PackageRegistry|Remove package"
msgstr "" msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet" msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr "" msgstr ""
...@@ -14144,6 +14150,9 @@ msgstr "" ...@@ -14144,6 +14150,9 @@ msgstr ""
msgid "PackageRegistry|There was a problem fetching the details for this package." msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr "" msgstr ""
msgid "PackageRegistry|To widen your search, change or remove the filters above."
msgstr ""
msgid "PackageRegistry|Unable to load package" msgid "PackageRegistry|Unable to load package"
msgstr "" msgstr ""
......
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