Commit 8ea42202 authored by jakeburden's avatar jakeburden

Use releases_sort component and fetch sorted releases

Add releases_sort component
Fetch over REST and GraphQL
parent 6ad5ae3e
......@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import ReleaseBlock from './release_block.vue';
import ReleasesPagination from './releases_pagination.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
import ReleasesSort from './releases_sort.vue';
export default {
name: 'ReleasesApp',
......@@ -16,6 +17,7 @@ export default {
ReleaseBlock,
ReleasesPagination,
ReleaseSkeletonLoader,
ReleasesSort,
},
computed: {
...mapState('list', [
......@@ -62,16 +64,20 @@ export default {
</script>
<template>
<div class="flex flex-column mt-2">
<gl-button
v-if="newReleasePath"
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
category="primary"
variant="success"
class="align-self-end mb-2 js-new-release-btn"
>
{{ __('New release') }}
</gl-button>
<div class="align-self-end mb-2">
<releases-sort class="gl-mr-2" @sort:changed="fetchReleases" />
<gl-button
v-if="newReleasePath"
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
category="primary"
variant="success"
class="js-new-release-btn"
>
{{ __('New release') }}
</gl-button>
</div>
<release-skeleton-loader v-if="isLoading" class="js-loading" />
......
<script>
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { ASCENDING_ODER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants';
export default {
name: 'ReleasesSort',
components: {
GlSorting,
GlSortingItem,
},
computed: {
...mapState('list', {
orderBy: state => state.sorting.orderBy,
sort: state => state.sorting.sort,
}),
sortOptions() {
return SORT_OPTIONS;
},
sortText() {
const option = this.sortOptions.find(s => s.orderBy === this.orderBy);
return option.label;
},
isSortAscending() {
return this.sort === ASCENDING_ODER;
},
},
methods: {
...mapActions('list', ['setSorting']),
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');
},
isActiveSortItem(item) {
const [itemOrderBy] = item.split(' ');
return this.orderBy === itemOrderBy;
},
},
};
</script>
<template>
<gl-sorting
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
v-for="item in sortOptions"
:key="item.orderBy"
:active="isActiveSortItem(item.orderBy)"
@click="onSortItemClick(item.orderBy)"
>
{{ item.label }}
</gl-sorting-item>
</gl-sorting>
</template>
import { __ } from '~/locale';
export const MAX_MILESTONES_TO_DISPLAY = 5;
export const BACK_URL_PARAM = 'back_url';
......@@ -12,3 +14,17 @@ export const ASSET_LINK_TYPE = Object.freeze({
export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER;
export const PAGE_SIZE = 20;
export const ASCENDING_ODER = 'asc';
export const DESCENDING_ORDER = 'desc';
export const SORT_OPTIONS = [
{
orderBy: 'released_at',
label: __('Released date'),
},
{
orderBy: 'created_at',
label: __('Created date'),
},
];
#import "./release.fragment.graphql"
query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String) {
query allReleases(
$fullPath: ID!
$first: Int
$last: Int
$before: String
$after: String
$sort: ReleaseSort
) {
project(fullPath: $fullPath) {
releases(first: $first, last: $last, before: $before, after: $after) {
releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) {
nodes {
...Release
}
......
......@@ -4,6 +4,7 @@ fragment Release on Release {
tagPath
descriptionHtml
releasedAt
createdAt
upcomingRelease
assets {
count
......
......@@ -42,6 +42,10 @@ export const fetchReleasesGraphQl = (
) => {
commit(types.REQUEST_RELEASES);
const { sort, orderBy } = state.sorting;
const orderByParam = orderBy === 'created_at' ? 'created' : orderBy;
const sortParams = `${orderByParam.toUpperCase()}_${sort.toUpperCase()}`;
let paginationParams;
if (!before && !after) {
paginationParams = { first: PAGE_SIZE };
......@@ -60,6 +64,7 @@ export const fetchReleasesGraphQl = (
query: allReleasesQuery,
variables: {
fullPath: state.projectPath,
sort: sortParams,
...paginationParams,
},
})
......@@ -80,8 +85,10 @@ export const fetchReleasesGraphQl = (
export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => {
commit(types.REQUEST_RELEASES);
const { sort, orderBy } = state.sorting;
api
.releases(state.projectId, { page })
.releases(state.projectId, { page, sort, order_by: orderBy })
.then(({ data, headers }) => {
const restPageInfo = parseIntPagination(normalizeHeaders(headers));
const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
......@@ -98,3 +105,5 @@ export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
createFlash(__('An error occurred while fetching the releases. Please try again.'));
};
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
export const REQUEST_RELEASES = 'REQUEST_RELEASES';
export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS';
export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR';
export const SET_SORTING = 'SET_SORTING';
......@@ -39,4 +39,8 @@ export default {
state.restPageInfo = {};
state.graphQlPageInfo = {};
},
[types.SET_SORTING](state, sorting) {
state.sorting = { ...state.sorting, ...sorting };
},
};
......@@ -16,4 +16,8 @@ export default ({
releases: [],
restPageInfo: {},
graphQlPageInfo: {},
sorting: {
sort: 'desc',
orderBy: 'released_at',
},
});
---
title: Implement gitlab-ui component for sorting Releases
merge_request: 43963
author:
type: added
......@@ -22326,6 +22326,9 @@ msgstr ""
msgid "ReleaseAssetLinkType|Runbooks"
msgstr ""
msgid "Released date"
msgstr ""
msgid "Releases"
msgstr ""
......
import Vuex from 'vuex';
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/releases/components/releases_sort.vue', () => {
let wrapper;
let store;
let sorting;
let listModule;
let sortingItems;
const projectId = 8;
const createComponent = () => {
listModule = createListModule({ projectId });
store = createStore({
modules: {
list: listModule,
},
});
store.dispatch = jest.fn();
wrapper = mount(ReleasesSort, {
store,
stubs: {
...stubChildren(ReleasesSort),
GlSortingItem,
},
localVue,
});
};
const findReleasesSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
beforeEach(() => {
createComponent();
sorting = findReleasesSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems).toHaveLength(wrapper.vm.sortOptions.length);
});
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('list/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.sortOptions[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('list/setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
......@@ -6,6 +6,7 @@ import {
fetchReleasesGraphQl,
fetchReleasesRest,
receiveReleasesError,
setSorting,
} from '~/releases/stores/modules/list/actions';
import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
......@@ -114,7 +115,7 @@ describe('Releases State actions', () => {
it('makes a GraphQl query with a first variable', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE },
variables: { fullPath: projectPath, first: PAGE_SIZE, sort: 'RELEASED_AT_DESC' },
});
});
});
......@@ -127,7 +128,7 @@ describe('Releases State actions', () => {
it('makes a GraphQl query with last and before variables', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, last: PAGE_SIZE, before },
variables: { fullPath: projectPath, last: PAGE_SIZE, before, sort: 'RELEASED_AT_DESC' },
});
});
});
......@@ -140,7 +141,7 @@ describe('Releases State actions', () => {
it('makes a GraphQl query with first and after variables', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE, after },
variables: { fullPath: projectPath, first: PAGE_SIZE, after, sort: 'RELEASED_AT_DESC' },
});
});
});
......@@ -156,6 +157,34 @@ describe('Releases State actions', () => {
);
});
});
describe('when the sort parameter is changed', () => {
beforeEach(() => {
mockedState.sorting.sort = 'asc';
fetchReleasesGraphQl(vuexParams, { before: undefined, after: undefined });
});
it('makes a GraphQL query with sort variable for the requested direction', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE, sort: 'RELEASED_AT_ASC' },
});
});
});
describe('when the orderBy parameter is changed', () => {
beforeEach(() => {
mockedState.sorting.orderBy = 'created_at';
fetchReleasesGraphQl(vuexParams, { before: undefined, after: undefined });
});
it('makes a GraphQl query with sort variable for the requested order', () => {
expect(gqClient.query).toHaveBeenCalledWith({
query: allReleasesQuery,
variables: { fullPath: projectPath, first: PAGE_SIZE, sort: 'CREATED_DESC' },
});
});
});
});
describe('when the request is successful', () => {
......@@ -230,7 +259,11 @@ describe('Releases State actions', () => {
});
it('makes a REST query with a page query parameter', () => {
expect(api.releases).toHaveBeenCalledWith(projectId, { page });
expect(api.releases).toHaveBeenCalledWith(projectId, {
page,
order_by: 'released_at',
sort: 'desc',
});
});
});
});
......@@ -302,4 +335,16 @@ describe('Releases State actions', () => {
);
});
});
describe('setSorting', () => {
it('should commit SET_SORTING', () => {
return testAction(
setSorting,
{ orderBy: 'released_at', sort: 'asc' },
null,
[{ type: types.SET_SORTING, payload: { orderBy: 'released_at', sort: 'asc' } }],
[],
);
});
});
});
......@@ -80,4 +80,16 @@ describe('Releases Store Mutations', () => {
expect(stateCopy.graphQlPageInfo).toEqual({});
});
});
describe('SET_SORTING', () => {
it('should merge the sorting object with sort value', () => {
mutations[types.SET_SORTING](stateCopy, { sort: 'asc' });
expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, sort: 'asc' });
});
it('should merge the sorting object with order_by value', () => {
mutations[types.SET_SORTING](stateCopy, { orderBy: 'created_at' });
expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, orderBy: 'created_at' });
});
});
});
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