Commit 8fe57fb5 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'jivavnl-add-pagination-jobs-refactor' into 'master'

Add pagination to the jobs table refactor

See merge request gitlab-org/gitlab!62338
parents 4b88d6a1 f34c24a7
export const GRAPHQL_PAGE_SIZE = 30;
export const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
first: GRAPHQL_PAGE_SIZE,
last: null,
};
query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) {
query getJobs(
$fullPath: ID!
$first: Int
$last: Int
$after: String
$before: String
$statuses: [CiJobStatus!]
) {
project(fullPath: $fullPath) {
jobs(first: 20, statuses: $statuses) {
jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) {
pageInfo {
endCursor
hasNextPage
......
<script>
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale';
import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
......@@ -12,6 +13,7 @@ export default {
},
components: {
GlAlert,
GlPagination,
GlSkeletonLoader,
JobsTable,
JobsTableEmptyState,
......@@ -28,10 +30,18 @@ export default {
variables() {
return {
fullPath: this.fullPath,
first: this.pagination.first,
last: this.pagination.last,
after: this.pagination.nextPageCursor,
before: this.pagination.prevPageCursor,
};
},
update({ project }) {
return project?.jobs?.nodes || [];
update(data) {
const { jobs: { nodes: list = [], pageInfo = {} } = {} } = data.project || {};
return {
list,
pageInfo,
};
},
error() {
this.hasError = true;
......@@ -40,10 +50,11 @@ export default {
},
data() {
return {
jobs: null,
jobs: {},
hasError: false,
isAlertDismissed: false,
scope: null,
pagination: initialPaginationState,
};
},
computed: {
......@@ -51,7 +62,16 @@ export default {
return this.hasError && !this.isAlertDismissed;
},
showEmptyState() {
return this.jobs.length === 0 && !this.scope;
return this.jobs.list.length === 0 && !this.scope;
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
return this.jobs.pageInfo?.hasNextPage ? this.pagination.currentPage + 1 : null;
},
showPaginationControls() {
return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading;
},
},
methods: {
......@@ -60,6 +80,24 @@ export default {
this.$apollo.queries.jobs.refetch({ statuses: scope });
},
handlePageChange(page) {
const { startCursor, endCursor } = this.jobs.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
last: GRAPHQL_PAGE_SIZE,
first: null,
prevPageCursor: startCursor,
currentPage: page,
};
}
},
},
};
</script>
......@@ -97,6 +135,16 @@ export default {
<jobs-table-empty-state v-else-if="showEmptyState" />
<jobs-table v-else :jobs="jobs" />
<jobs-table v-else :jobs="jobs.list" />
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-mt-3"
@input="handlePageChange"
/>
</div>
</template>
import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui';
import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -25,6 +25,10 @@ describe('Job table app', () => {
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPagination = () => wrapper.findComponent(GlPagination);
const findPrevious = () => findPagination().findAll('.page-item').at(0);
const findNext = () => findPagination().findAll('.page-item').at(1);
const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]];
......@@ -32,8 +36,17 @@ describe('Job table app', () => {
return createMockApollo(requestHandlers);
};
const createComponent = (handler = successHandler, mountFn = shallowMount) => {
const createComponent = ({
handler = successHandler,
mountFn = shallowMount,
data = {},
} = {}) => {
wrapper = mountFn(JobsTableApp, {
data() {
return {
...data,
};
},
provide: {
projectPath,
},
......@@ -52,6 +65,7 @@ describe('Job table app', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
});
});
......@@ -65,9 +79,10 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
});
it('should retfech jobs query on fetchJobsByStatus event', async () => {
it('should refetch jobs query on fetchJobsByStatus event', async () => {
jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
......@@ -78,9 +93,72 @@ describe('Job table app', () => {
});
});
describe('pagination', () => {
it('should disable the next page button on the last page', async () => {
createComponent({
handler: successHandler,
mountFn: mount,
data: {
pagination: {
currentPage: 3,
},
jobs: {
pageInfo: {
hasPreviousPage: true,
startCursor: 'abc',
endCursor: 'bcd',
},
},
},
});
await wrapper.vm.$nextTick();
wrapper.setData({
jobs: {
pageInfo: {
hasNextPage: false,
},
},
});
await wrapper.vm.$nextTick();
expect(findPrevious().exists()).toBe(true);
expect(findNext().exists()).toBe(true);
expect(findNext().classes('disabled')).toBe(true);
});
it('should disable the previous page button on the first page', async () => {
createComponent({
handler: successHandler,
mountFn: mount,
data: {
pagination: {
currentPage: 1,
},
jobs: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'abc',
endCursor: 'bcd',
},
},
},
});
await wrapper.vm.$nextTick();
expect(findPrevious().exists()).toBe(true);
expect(findPrevious().classes('disabled')).toBe(true);
expect(findNext().exists()).toBe(true);
});
});
describe('error state', () => {
it('should show an alert if there is an error fetching the data', async () => {
createComponent(failedHandler);
createComponent({ handler: failedHandler });
await waitForPromises();
......@@ -90,7 +168,7 @@ describe('Job table app', () => {
describe('empty state', () => {
it('should display empty state if there are no jobs and tab scope is null', async () => {
createComponent(emptyHandler, mount);
createComponent({ handler: emptyHandler, mountFn: mount });
await waitForPromises();
......@@ -99,7 +177,7 @@ describe('Job table app', () => {
});
it('should not display empty state if there are jobs and tab scope is not null', async () => {
createComponent(successHandler, mount);
createComponent({ handler: successHandler, mountFn: mount });
await waitForPromises();
......
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