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 @@
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue';
import { PACKAGE_REGISTRY_TABS } from '../constants';
......@@ -11,17 +12,21 @@ export default {
GlEmptyState,
GlTab,
GlTabs,
PackageFilter,
PackageList,
PackageSort,
},
computed: {
...mapState({
resourceId: state => state.config.resourceId,
emptyListIllustration: state => state.config.emptyListIllustration,
emptyListHelpUrl: state => state.config.emptyListHelpUrl,
totalItems: state => state.pagination.total,
filterQuery: state => state.filterQuery,
}),
emptyListText() {
if (this.filterQuery) {
return s__('PackageRegistry|To widen your search, change or remove the filters above.');
}
return sprintf(
s__(
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
......@@ -55,6 +60,10 @@ export default {
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,
......@@ -70,7 +79,8 @@ export default {
<template>
<gl-tabs @input="tabChanged">
<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" />
</div>
</template>
......
......@@ -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 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 receivePackagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_PACKAGE_LIST_SUCCESS, data);
......@@ -26,12 +27,15 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting;
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';
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 }) => {
dispatch('receivePackagesListSuccess', { data, headers });
......
......@@ -5,3 +5,4 @@ export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING';
export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
export const SET_FILTER = 'SET_FILTER';
......@@ -30,4 +30,8 @@ export default {
[types.SET_SELECTED_TYPE](state, type) {
state.selectedType = type;
},
[types.SET_FILTER](state, query) {
state.filterQuery = query;
},
};
......@@ -43,4 +43,8 @@ export default () => ({
sort: 'desc',
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`] = `
</template>
<template>
<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 />
</div>
</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', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
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 = () => {
wrapper = shallowMount(PackageListApp, {
localVue,
......@@ -35,17 +51,7 @@ describe('packages_list_app', () => {
};
beforeEach(() => {
store = new Vuex.Store({
state: {
isLoading: false,
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
},
},
});
store.dispatch = jest.fn();
createStore();
mountComponent();
});
......@@ -73,7 +79,7 @@ describe('packages_list_app', () => {
});
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');
});
......@@ -110,4 +116,18 @@ describe('packages_list_app', () => {
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', () => {
);
});
});
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', () => {
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 ""
msgid "PackageRegistry|Delete package"
msgstr ""
msgid "PackageRegistry|Filter by name"
msgstr ""
msgid "PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
......@@ -14135,6 +14138,9 @@ msgstr ""
msgid "PackageRegistry|Remove package"
msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
......@@ -14144,6 +14150,9 @@ msgstr ""
msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr ""
msgid "PackageRegistry|To widen your search, change or remove the filters above."
msgstr ""
msgid "PackageRegistry|Unable to load package"
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