Commit f7272b77 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '229406-incident-pagination' into 'master'

Add incident pagination

See merge request gitlab-org/gitlab!37993
parents 9a5e78c4 23e6e69a
......@@ -10,13 +10,14 @@ import {
GlButton,
GlSearchBoxByType,
GlIcon,
GlPagination,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import { I18N, INCIDENT_SEARCH_DELAY } from '../constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY } from '../constants';
const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
......@@ -24,6 +25,14 @@ const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: DEFAULT_PAGE_SIZE,
lastPageSize: null,
};
export default {
i18n: I18N,
fields: [
......@@ -57,6 +66,7 @@ export default {
TimeAgoTooltip,
GlSearchBoxByType,
GlIcon,
GlPagination,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -70,9 +80,18 @@ export default {
searchTerm: this.searchTerm,
projectPath: this.projectPath,
labelNames: ['incident'],
firstPageSize: this.pagination.firstPageSize,
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
};
},
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
return {
list: nodes,
pageInfo,
};
},
update: ({ project: { issues: { nodes = [] } = {} } = {} }) => nodes,
error() {
this.errored = true;
},
......@@ -84,6 +103,8 @@ export default {
isErrorAlertDismissed: false,
redirecting: false,
searchTerm: '',
pagination: initialPaginationState,
incidents: {},
};
},
computed: {
......@@ -94,7 +115,19 @@ export default {
return this.$apollo.queries.incidents.loading;
},
hasIncidents() {
return this.incidents?.length;
return this.incidents?.list?.length;
},
showPaginationControls() {
return Boolean(
this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
);
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
return this.incidents?.list?.length < DEFAULT_PAGE_SIZE ? null : nextPage;
},
tbodyTrClass() {
return {
......@@ -119,6 +152,28 @@ export default {
navigateToIncidentDetails({ iid }) {
return visitUrl(joinPaths(this.issuePath, iid));
},
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
lastPageSize: DEFAULT_PAGE_SIZE,
firstPageSize: null,
prevPageCursor: startCursor,
nextPageCursor: '',
currentPage: page,
};
}
},
resetPagination() {
this.pagination = initialPaginationState;
},
},
};
</script>
......@@ -155,7 +210,7 @@ export default {
{{ s__('IncidentManagement|Incidents') }}
</h4>
<gl-table
:items="incidents"
:items="incidents.list || []"
:fields="$options.fields"
:show-empty="true"
:busy="loading"
......@@ -219,5 +274,15 @@ export default {
{{ $options.i18n.noIncidents }}
</template>
</gl-table>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</div>
</template>
......@@ -9,3 +9,4 @@ export const I18N = {
};
export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 10;
query getIncidents(
$searchTerm: String
$projectPath: ID!
$labelNames: [String]
$state: IssuableState
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
$searchTerm: String
) {
project(fullPath: $projectPath) {
issues(search: $searchTerm, state: $state, labelName: $labelNames) {
issues(
search: $searchTerm
state: $state
labelName: $labelNames
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
) {
nodes {
iid
title
......@@ -26,6 +38,12 @@ query getIncidents(
}
}
}
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
}
}
}
---
title: Add pagination to the incident list
merge_request: 37993
author:
type: changed
import { mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable, GlAvatar, GlSearchBoxByType } from '@gitlab/ui';
import {
GlAlert,
GlLoadingIcon,
GlTable,
GlAvatar,
GlPagination,
GlSearchBoxByType,
} from '@gitlab/ui';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -26,6 +33,7 @@ describe('Incidents List', () => {
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findSearch = () => wrapper.find(GlSearchBoxByType);
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findPagination = () => wrapper.find(GlPagination);
function mountComponent({ data = { incidents: [] }, loading = false }) {
wrapper = mount(IncidentsList, {
......@@ -70,7 +78,7 @@ describe('Incidents List', () => {
it('shows empty state', () => {
mountComponent({
data: { incidents: [] },
data: { incidents: { list: [] } },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
......@@ -78,7 +86,7 @@ describe('Incidents List', () => {
it('shows error state', () => {
mountComponent({
data: { incidents: [], errored: true },
data: { incidents: { list: [] }, errored: true },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
......@@ -88,7 +96,7 @@ describe('Incidents List', () => {
describe('Incident Management list', () => {
beforeEach(() => {
mountComponent({
data: { incidents: mockIncidents },
data: { incidents: { list: mockIncidents } },
loading: false,
});
});
......@@ -140,7 +148,7 @@ describe('Incidents List', () => {
describe('Create Incident', () => {
beforeEach(() => {
mountComponent({
data: { incidents: [] },
data: { incidents: { list: [] } },
loading: false,
});
});
......@@ -157,24 +165,120 @@ describe('Incidents List', () => {
});
});
describe('Search', () => {
describe('Pagination', () => {
beforeEach(() => {
mountComponent({
data: { incidents: mockIncidents },
data: {
incidents: {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false,
});
});
it('renders the search component for incidents', () => {
expect(findSearch().exists()).toBe(true);
it('should render pagination', () => {
expect(wrapper.find(GlPagination).exists()).toBe(true);
});
describe('prevPage', () => {
it('returns prevPage button', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(0)
.text(),
).toBe('Prev');
});
});
it('returns prevPage number', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(2);
});
});
it('returns 0 when it is the first page', () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(0);
});
});
});
describe('nextPage', () => {
it('returns nextPage button', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(1)
.text(),
).toBe('Next');
});
});
it('returns nextPage number', () => {
mountComponent({
data: {
incidents: {
list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false,
});
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBe(2);
});
});
it('returns `null` when currentPage is already last page', () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
});
});
});
it('sets the `searchTerm` graphql variable', () => {
const SEARCH_TERM = 'Simple Incident';
describe('Search', () => {
beforeEach(() => {
mountComponent({
data: {
incidents: {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false,
});
});
findSearch().vm.$emit('input', SEARCH_TERM);
it('renders the search component for incidents', () => {
expect(findSearch().exists()).toBe(true);
});
it('sets the `searchTerm` graphql variable', () => {
const SEARCH_TERM = 'Simple Incident';
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
findSearch().vm.$emit('input', SEARCH_TERM);
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
});
});
});
});
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