Commit 878a6bdb authored by Phil Hughes's avatar Phil Hughes

Merge branch '290302-update-the-default-sort-order-of-the-image-repository-list' into 'master'

Extract package_search to be re-usable

See merge request gitlab-org/gitlab!53457
parents 88c98896 41cf4507
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import getTableHeaders from '../utils';
import PackageTypeToken from './tokens/package_type_token.vue';
export default {
components: {
GlSorting,
GlSortingItem,
GlFilteredSearch,
},
tokens: [
{
type: 'type',
icon: 'package',
title: s__('PackageRegistry|Type'),
unique: true,
token: PackageTypeToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
},
],
components: { RegistrySearch },
computed: {
...mapState({
isGroupPage: (state) => state.config.isGroupPage,
orderBy: (state) => state.sorting.orderBy,
sort: (state) => state.sorting.sort,
sorting: (state) => state.sorting,
filter: (state) => state.filter,
}),
internalFilter: {
get() {
return this.filter;
},
set(value) {
this.setFilter(value);
},
},
sortText() {
const field = this.sortableFields.find((s) => s.orderBy === this.orderBy);
return field ? field.label : '';
},
sortableFields() {
return getTableHeaders(this.isGroupPage);
},
isSortAscending() {
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: {
...mapActions(['setSorting', 'setFilter']),
onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort });
this.$emit('sort:changed');
},
onSortItemClick(item) {
this.setSorting({ orderBy: item });
this.$emit('sort:changed');
},
clearSearch() {
this.setFilter([]);
this.$emit('filter:changed');
updateSorting(newValue) {
this.setSorting(newValue);
this.$emit('update');
},
},
};
</script>
<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
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
v-for="item in sortableFields"
ref="packageListSortItem"
:key="item.orderBy"
@click="onSortItemClick(item.orderBy)"
>
{{ item.label }}
</gl-sorting-item>
</gl-sorting>
</div>
<registry-search
:filter="filter"
:sorting="sorting"
:tokens="$options.tokens"
:sortable-fields="sortableFields"
@sorting:changed="updateSorting"
@filter:changed="setFilter"
@filter:submit="$emit('update')"
/>
</template>
......@@ -75,7 +75,7 @@ export default {
<template>
<div>
<package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" />
<package-search @sort:changed="requestPackagesList" @filter:changed="requestPackagesList" />
<package-search @update="requestPackagesList" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
......
......@@ -26,9 +26,6 @@ export const LIST_LABEL_PACKAGE_TYPE = __('Type');
export const LIST_LABEL_CREATED_AT = __('Published');
export const LIST_LABEL_ACTIONS = '';
export const ASCENDING_ODER = 'asc';
export const DESCENDING_ORDER = 'desc';
// The following is not translated because it is used to build a JavaScript exception error message
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
......
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
const ASCENDING_ORDER = 'asc';
const DESCENDING_ORDER = 'desc';
export default {
components: {
GlSorting,
GlSortingItem,
GlFilteredSearch,
},
props: {
filter: {
type: Array,
required: true,
},
sorting: {
type: Object,
required: true,
},
tokens: {
type: Array,
required: false,
default: () => [],
},
sortableFields: {
type: Array,
required: true,
},
},
computed: {
internalFilter: {
get() {
return this.filter;
},
set(value) {
this.$emit('filter:changed', value);
},
},
sortText() {
const field = this.sortableFields.find((s) => s.orderBy === this.sorting.orderBy);
return field ? field.label : '';
},
isSortAscending() {
return this.sorting.sort === ASCENDING_ORDER;
},
},
methods: {
onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
this.$emit('sorting:changed', { sort });
},
onSortItemClick(item) {
this.$emit('sorting:changed', { orderBy: item });
},
clearSearch() {
this.$emit('filter:changed', []);
this.$emit('filter:submit');
},
},
};
</script>
<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:submit')"
@clear="clearSearch"
/>
<gl-sorting
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
v-for="item in sortableFields"
ref="packageListSortItem"
:key="item.orderBy"
@click="onSortItemClick(item.orderBy)"
>
{{ item.label }}
</gl-sorting-item>
</gl-sorting>
</div>
</template>
......@@ -93,6 +93,7 @@ describe('packages_list_app', () => {
it('call requestPackagesList on page:changed', () => {
mountComponent();
store.dispatch.mockClear();
const list = findListComponent();
list.vm.$emit('page:changed', 1);
......@@ -107,14 +108,6 @@ describe('packages_list_app', () => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
it('calls requestPackagesList on sort:changed', () => {
mountComponent();
const list = findListComponent();
list.vm.$emit('sort:changed');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
it('does not call requestPackagesList two times on render', () => {
mountComponent();
......@@ -142,10 +135,11 @@ describe('packages_list_app', () => {
expect(findPackageSearch().exists()).toBe(true);
});
it.each(['sort:changed', 'filter:changed'])('on %p fetches data from the store', (event) => {
it('on update fetches data from the store', () => {
mountComponent();
store.dispatch.mockClear();
findPackageSearch().vm.$emit(event);
findPackageSearch().vm.$emit('update');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
......
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 RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
import getTableHeaders from '~/packages/list/utils';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -10,12 +11,8 @@ 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 findRegistrySearch = () => wrapper.find(RegistrySearch);
const createStore = (isGroupPage) => {
const state = {
......@@ -40,9 +37,6 @@ describe('Package Search', () => {
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlSortingItem,
},
});
};
......@@ -51,95 +45,63 @@ describe('Package Search', () => {
wrapper = null;
});
describe('searching', () => {
it('has a filtered-search component', () => {
mountComponent();
expect(findFilteredSearch().exists()).toBe(true);
it('has a registry search component', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(true);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: getTableHeaders(),
});
});
it('binds the correct props to filtered-search', () => {
mountComponent();
expect(findFilteredSearch().props()).toMatchObject({
value: [],
placeholder: 'Filter results',
availableTokens: wrapper.vm.tokens,
});
it.each`
isGroupPage | page
${false} | ${'project'}
${true} | ${'group'}
`('in a $page page binds the right props', ({ isGroupPage }) => {
mountComponent(isGroupPage);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: getTableHeaders(isGroupPage),
});
});
it('updates vuex when value changes', () => {
mountComponent();
findFilteredSearch().vm.$emit('input', ['foo']);
expect(store.dispatch).toHaveBeenCalledWith('setFilter', ['foo']);
});
it('on sorting:changed emits update event and calls vuex setSorting', () => {
const payload = { sort: 'foo' };
it('emits filter:changed on submit event', () => {
mountComponent();
mountComponent();
findFilteredSearch().vm.$emit('submit');
expect(wrapper.emitted('filter:changed')).toEqual([[]]);
});
findRegistrySearch().vm.$emit('sorting:changed', payload);
it('emits filter:changed on clear event and reset vuex', () => {
mountComponent();
expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
expect(wrapper.emitted('update')).toEqual([[]]);
});
findFilteredSearch().vm.$emit('clear');
it('on filter:changed calls vuex setFilter', () => {
const payload = ['foo'];
expect(store.dispatch).toHaveBeenCalledWith('setFilter', []);
expect(wrapper.emitted('filter:changed')).toEqual([[]]);
});
mountComponent();
it('has a PackageTypeToken token', () => {
mountComponent();
findRegistrySearch().vm.$emit('filter:changed', payload);
expect(findFilteredSearch().props('availableTokens')).toEqual(
expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
);
});
expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
});
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();
});
});
it('on filter:submit emits update event', () => {
mountComponent();
describe('when is in group', () => {
beforeEach(() => {
mountComponent(true);
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
findRegistrySearch().vm.$emit('filter:submit');
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
});
});
expect(wrapper.emitted('update')).toEqual([[]]);
});
});
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import component from '~/vue_shared/components/registry/registry_search.vue';
describe('Registry Search', () => {
let wrapper;
const findPackageListSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const defaultProps = {
filter: [],
sorting: { sort: 'asc', orderBy: 'name' },
tokens: ['foo'],
sortableFields: [{ label: 'name', orderBy: 'name' }, { label: 'baz' }],
};
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
propsData,
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('emits filter:changed when value changes', () => {
mountComponent();
findFilteredSearch().vm.$emit('input', 'foo');
expect(wrapper.emitted('filter:changed')).toEqual([['foo']]);
});
it('emits filter:submit on submit event', () => {
mountComponent();
findFilteredSearch().vm.$emit('submit');
expect(wrapper.emitted('filter:submit')).toEqual([[]]);
});
it('emits filter:changed and filter:submit on clear event', () => {
mountComponent();
findFilteredSearch().vm.$emit('clear');
expect(wrapper.emitted('filter:changed')).toEqual([[[]]]);
expect(wrapper.emitted('filter:submit')).toEqual([[]]);
});
it('binds tokens prop', () => {
mountComponent();
expect(findFilteredSearch().props('availableTokens')).toEqual(defaultProps.tokens);
});
});
describe('sorting', () => {
it('has all the sortable items', () => {
mountComponent();
expect(findSortingItems()).toHaveLength(defaultProps.sortableFields.length);
});
it('on sort change emits sorting:changed event', () => {
mountComponent();
findPackageListSorting().vm.$emit('sortDirectionChange');
expect(wrapper.emitted('sorting:changed')).toEqual([[{ sort: 'desc' }]]);
});
it('on sort item click emits sorting:changed event ', () => {
mountComponent();
findSortingItems().at(0).vm.$emit('click');
expect(wrapper.emitted('sorting:changed')).toEqual([
[{ orderBy: defaultProps.sortableFields[0].orderBy }],
]);
});
});
});
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