Commit def2c6f7 authored by Nathan Friend's avatar Nathan Friend Committed by Vitaly Slobodin

Add pagination subcomponents for "Releases" page

This commit adds three new components that will be used (in a future MR)
to replace the
current pagination controls on the "Releases" page.
parent 4e4f7da7
<script>
import { mapGetters } from 'vuex';
import ReleasesPaginationGraphql from './releases_pagination_graphql.vue';
import ReleasesPaginationRest from './releases_pagination_rest.vue';
export default {
name: 'ReleasesPagination',
components: { ReleasesPaginationGraphql, ReleasesPaginationRest },
computed: {
...mapGetters(['useGraphQLEndpoint']),
},
};
</script>
<template>
<div class="gl-display-flex gl-justify-content-center">
<releases-pagination-graphql v-if="useGraphQLEndpoint" />
<releases-pagination-rest v-else />
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlKeysetPagination } from '@gitlab/ui';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
name: 'ReleasesPaginationGraphql',
components: { GlKeysetPagination },
computed: {
...mapState('list', ['projectPath', 'graphQlPageInfo']),
showPagination() {
return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage;
},
},
methods: {
...mapActions('list', ['fetchReleasesGraphQl']),
onPrev(before) {
historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
this.fetchReleasesGraphQl({ projectPath: this.projectPath, before });
},
onNext(after) {
historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
this.fetchReleasesGraphQl({ projectPath: this.projectPath, after });
},
},
};
</script>
<template>
<gl-keyset-pagination
v-if="showPagination"
v-bind="graphQlPageInfo"
@prev="onPrev($event)"
@next="onNext($event)"
/>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
name: 'ReleasesPaginationRest',
components: { TablePagination },
computed: {
...mapState('list', ['projectId', 'pageInfo']),
},
methods: {
...mapActions('list', ['fetchReleasesRest']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
this.fetchReleasesRest({ page, projectId: this.projectId });
},
},
};
</script>
<template>
<table-pagination :change="onChangePage" :page-info="pageInfo" />
</template>
import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-edit-release-page');
......
import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseListApp from './components/app_index.vue';
import createStore from './stores';
import listModule from './stores/modules/list';
import createListModule from './stores/modules/list';
Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-releases-page');
......@@ -10,7 +13,7 @@ export default () => {
el,
store: createStore({
modules: {
list: listModule,
list: createListModule(el.dataset),
},
featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
......
import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-new-release-page');
......
import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-show-release-page');
......
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
......
import state from './state';
import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
export default {
export default initialState => ({
namespaced: true,
actions,
mutations,
state,
};
state: createState(initialState),
});
export default () => ({
export default ({
projectId,
projectPath,
documentationPath,
illustrationPath,
newReleasePath = '',
}) => ({
projectId,
projectPath,
documentationPath,
illustrationPath,
newReleasePath,
isLoading: false,
hasError: false,
releases: [],
......
......@@ -4,7 +4,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import listModule from '~/releases/stores/modules/list';
import createListModule from '~/releases/stores/modules/list';
import api from '~/api';
import {
pageInfoHeadersWithoutPagination,
......@@ -35,6 +35,8 @@ describe('Releases App ', () => {
};
const createComponent = (propsData = defaultProps) => {
const listModule = createListModule({});
fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases');
const store = createStore({
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
import { historyPushState } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
historyPushState: jest.fn(),
}));
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/releases/components/releases_pagination_graphql.vue', () => {
let wrapper;
let listModule;
const cursors = {
startCursor: 'startCursor',
endCursor: 'endCursor',
};
const projectPath = 'my/project';
const createComponent = pageInfo => {
listModule = createListModule({ projectPath });
listModule.state.graphQlPageInfo = pageInfo;
listModule.actions.fetchReleasesGraphQl = jest.fn();
wrapper = mount(ReleasesPaginationGraphql, {
store: createStore({
modules: {
list: listModule,
},
featureFlags: {},
}),
localVue,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findPrevButton = () => wrapper.find('[data-testid="prevButton"]');
const findNextButton = () => wrapper.find('[data-testid="nextButton"]');
const expectDisabledPrev = () => {
expect(findPrevButton().attributes().disabled).toBe('disabled');
};
const expectEnabledPrev = () => {
expect(findPrevButton().attributes().disabled).toBe(undefined);
};
const expectDisabledNext = () => {
expect(findNextButton().attributes().disabled).toBe('disabled');
};
const expectEnabledNext = () => {
expect(findNextButton().attributes().disabled).toBe(undefined);
};
describe('when there is only one page of results', () => {
beforeEach(() => {
createComponent({
hasPreviousPage: false,
hasNextPage: false,
});
});
it('does not render anything', () => {
expect(wrapper.isEmpty()).toBe(true);
});
});
describe('when there is a next page, but not a previous page', () => {
beforeEach(() => {
createComponent({
hasPreviousPage: false,
hasNextPage: true,
});
});
it('renders a disabled "Prev" button', () => {
expectDisabledPrev();
});
it('renders an enabled "Next" button', () => {
expectEnabledNext();
});
});
describe('when there is a previous page, but not a next page', () => {
beforeEach(() => {
createComponent({
hasPreviousPage: true,
hasNextPage: false,
});
});
it('renders a enabled "Prev" button', () => {
expectEnabledPrev();
});
it('renders an disabled "Next" button', () => {
expectDisabledNext();
});
});
describe('when there is both a previous page and a next page', () => {
beforeEach(() => {
createComponent({
hasPreviousPage: true,
hasNextPage: true,
});
});
it('renders a enabled "Prev" button', () => {
expectEnabledPrev();
});
it('renders an enabled "Next" button', () => {
expectEnabledNext();
});
});
describe('button behavior', () => {
beforeEach(() => {
createComponent({
hasPreviousPage: true,
hasNextPage: true,
...cursors,
});
});
describe('next button behavior', () => {
beforeEach(() => {
findNextButton().trigger('click');
});
it('calls fetchReleasesGraphQl with the correct after cursor', () => {
expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
[expect.anything(), { projectPath, after: cursors.endCursor }],
]);
});
it('calls historyPushState with the new URL', () => {
expect(historyPushState.mock.calls).toEqual([
[expect.stringContaining(`?after=${cursors.endCursor}`)],
]);
});
});
describe('previous button behavior', () => {
beforeEach(() => {
findPrevButton().trigger('click');
});
it('calls fetchReleasesGraphQl with the correct before cursor', () => {
expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
[expect.anything(), { projectPath, before: cursors.startCursor }],
]);
});
it('calls historyPushState with the new URL', () => {
expect(historyPushState.mock.calls).toEqual([
[expect.stringContaining(`?before=${cursors.startCursor}`)],
]);
});
});
});
});
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
import * as commonUtils from '~/lib/utils/common_utils';
commonUtils.historyPushState = jest.fn();
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/releases/components/releases_pagination_rest.vue', () => {
let wrapper;
let listModule;
const projectId = 19;
const createComponent = pageInfo => {
listModule = createListModule({ projectId });
listModule.state.pageInfo = pageInfo;
listModule.actions.fetchReleasesRest = jest.fn();
wrapper = mount(ReleasesPaginationRest, {
store: createStore({
modules: {
list: listModule,
},
featureFlags: {},
}),
localVue,
});
};
const findGlPagination = () => wrapper.find(GlPagination);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when a page number is clicked', () => {
const newPage = 2;
beforeEach(() => {
createComponent({
perPage: 20,
page: 1,
total: 40,
totalPages: 2,
nextPage: 2,
});
findGlPagination().vm.$emit('input', newPage);
});
it('calls fetchReleasesRest with the correct page', () => {
expect(listModule.actions.fetchReleasesRest.mock.calls).toEqual([
[expect.anything(), { projectId, page: newPage }],
]);
});
it('calls historyPushState with the new URL', () => {
expect(commonUtils.historyPushState.mock.calls).toEqual([
[expect.stringContaining(`?page=${newPage}`)],
]);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/releases/components/releases_pagination.vue', () => {
let wrapper;
const createComponent = useGraphQLEndpoint => {
const store = new Vuex.Store({
getters: {
useGraphQLEndpoint: () => useGraphQLEndpoint,
},
});
wrapper = shallowMount(ReleasesPagination, { store, localVue });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findRestPagination = () => wrapper.find(ReleasesPaginationRest);
const findGraphQlPagination = () => wrapper.find(ReleasesPaginationGraphql);
describe('when one of necessary feature flags is disabled', () => {
beforeEach(() => {
createComponent(false);
});
it('renders the REST pagination component', () => {
expect(findRestPagination().exists()).toBe(true);
expect(findGraphQlPagination().exists()).toBe(false);
});
});
describe('when all the necessary feature flags are enabled', () => {
beforeEach(() => {
createComponent(true);
});
it('renders the GraphQL pagination component', () => {
expect(findGraphQlPagination().exists()).toBe(true);
expect(findRestPagination().exists()).toBe(false);
});
});
});
import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
......@@ -7,6 +8,9 @@ import createDetailModule from '~/releases/stores/modules/detail';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_DOCS_PATH = '/help/test/docs/path';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('releases/components/tag_field_existing', () => {
let store;
let wrapper;
......@@ -14,6 +18,7 @@ describe('releases/components/tag_field_existing', () => {
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldExisting, {
store,
localVue,
});
};
......
......@@ -6,7 +6,7 @@ import {
receiveReleasesSuccess,
receiveReleasesError,
} from '~/releases/stores/modules/list/actions';
import state from '~/releases/stores/modules/list/state';
import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util';
......@@ -27,7 +27,7 @@ describe('Releases State actions', () => {
beforeEach(() => {
mockedState = {
...state(),
...createState({}),
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: true,
......
import state from '~/releases/stores/modules/list/state';
import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils';
......@@ -9,7 +9,7 @@ describe('Releases Store Mutations', () => {
let pageInfo;
beforeEach(() => {
stateCopy = state();
stateCopy = createState({});
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
});
......
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