Commit 2b657852 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'nfriend-add-releases-pagination-components' into 'master'

Step 2/4: Add pagination sub-components for "Releases" page

See merge request gitlab-org/gitlab!41621
parents e9d54c99 def2c6f7
<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 Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue'; import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores'; import createStore from './stores';
import createDetailModule from './stores/modules/detail'; import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => { export default () => {
const el = document.getElementById('js-edit-release-page'); const el = document.getElementById('js-edit-release-page');
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseListApp from './components/app_index.vue'; import ReleaseListApp from './components/app_index.vue';
import createStore from './stores'; import createStore from './stores';
import listModule from './stores/modules/list'; import createListModule from './stores/modules/list';
Vue.use(Vuex);
export default () => { export default () => {
const el = document.getElementById('js-releases-page'); const el = document.getElementById('js-releases-page');
...@@ -10,7 +13,7 @@ export default () => { ...@@ -10,7 +13,7 @@ export default () => {
el, el,
store: createStore({ store: createStore({
modules: { modules: {
list: listModule, list: createListModule(el.dataset),
}, },
featureFlags: { featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData), graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue'; import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores'; import createStore from './stores';
import createDetailModule from './stores/modules/detail'; import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => { export default () => {
const el = document.getElementById('js-new-release-page'); const el = document.getElementById('js-new-release-page');
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseShowApp from './components/app_show.vue'; import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores'; import createStore from './stores';
import createDetailModule from './stores/modules/detail'; import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
export default () => { export default () => {
const el = document.getElementById('js-show-release-page'); const el = document.getElementById('js-show-release-page');
......
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
Vue.use(Vuex);
export default ({ modules, featureFlags }) => export default ({ modules, featureFlags }) =>
new Vuex.Store({ new Vuex.Store({
modules, modules,
......
import state from './state'; import createState from './state';
import * as actions from './actions'; import * as actions from './actions';
import mutations from './mutations'; import mutations from './mutations';
export default { export default initialState => ({
namespaced: true, namespaced: true,
actions, actions,
mutations, mutations,
state, state: createState(initialState),
}; });
export default () => ({ export default ({
projectId,
projectPath,
documentationPath,
illustrationPath,
newReleasePath = '',
}) => ({
projectId,
projectPath,
documentationPath,
illustrationPath,
newReleasePath,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
releases: [], releases: [],
......
...@@ -4,7 +4,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -4,7 +4,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import ReleasesApp from '~/releases/components/app_index.vue'; import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores'; import createStore from '~/releases/stores';
import listModule from '~/releases/stores/modules/list'; import createListModule from '~/releases/stores/modules/list';
import api from '~/api'; import api from '~/api';
import { import {
pageInfoHeadersWithoutPagination, pageInfoHeadersWithoutPagination,
...@@ -35,6 +35,8 @@ describe('Releases App ', () => { ...@@ -35,6 +35,8 @@ describe('Releases App ', () => {
}; };
const createComponent = (propsData = defaultProps) => { const createComponent = (propsData = defaultProps) => {
const listModule = createListModule({});
fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases'); fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases');
const store = createStore({ 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 { 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 TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores'; import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail'; import createDetailModule from '~/releases/stores/modules/detail';
...@@ -7,6 +8,9 @@ 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_TAG_NAME = 'test-tag-name';
const TEST_DOCS_PATH = '/help/test/docs/path'; const TEST_DOCS_PATH = '/help/test/docs/path';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('releases/components/tag_field_existing', () => { describe('releases/components/tag_field_existing', () => {
let store; let store;
let wrapper; let wrapper;
...@@ -14,6 +18,7 @@ describe('releases/components/tag_field_existing', () => { ...@@ -14,6 +18,7 @@ describe('releases/components/tag_field_existing', () => {
const createComponent = (mountFn = shallowMount) => { const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldExisting, { wrapper = mountFn(TagFieldExisting, {
store, store,
localVue,
}); });
}; };
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
receiveReleasesSuccess, receiveReleasesSuccess,
receiveReleasesError, receiveReleasesError,
} from '~/releases/stores/modules/list/actions'; } 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 * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api'; import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util'; import { gqClient, convertGraphQLResponse } from '~/releases/util';
...@@ -27,7 +27,7 @@ describe('Releases State actions', () => { ...@@ -27,7 +27,7 @@ describe('Releases State actions', () => {
beforeEach(() => { beforeEach(() => {
mockedState = { mockedState = {
...state(), ...createState({}),
featureFlags: { featureFlags: {
graphqlReleaseData: true, graphqlReleaseData: true,
graphqlReleasesPage: 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 mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types'; import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils'; import { parseIntPagination } from '~/lib/utils/common_utils';
...@@ -9,7 +9,7 @@ describe('Releases Store Mutations', () => { ...@@ -9,7 +9,7 @@ describe('Releases Store Mutations', () => {
let pageInfo; let pageInfo;
beforeEach(() => { beforeEach(() => {
stateCopy = state(); stateCopy = createState({});
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); 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