Commit 9c0b873f authored by Kushal Pandya's avatar Kushal Pandya

Use pagination to load epics in roadmap

Use pagination via IntersectionObserver to load
epics in roadmap on scroll instead of loading all
epics at once.
parent 100fa268
<script> <script>
import { GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
...@@ -13,6 +14,8 @@ export default { ...@@ -13,6 +14,8 @@ export default {
EpicItem, EpicItem,
epicItemHeight: EPIC_ITEM_HEIGHT, epicItemHeight: EPIC_ITEM_HEIGHT,
components: { components: {
GlIntersectionObserver,
GlLoadingIcon,
EpicItem, EpicItem,
CurrentDayIndicator, CurrentDayIndicator,
}, },
...@@ -49,7 +52,15 @@ export default { ...@@ -49,7 +52,15 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['bufferSize', 'epicIid', 'childrenEpics', 'childrenFlags', 'epicIds']), ...mapState([
'bufferSize',
'epicIid',
'childrenEpics',
'childrenFlags',
'epicIds',
'pageInfo',
'epicsFetchForNextPageInProgress',
]),
emptyRowContainerVisible() { emptyRowContainerVisible() {
return this.displayedEpics.length < this.bufferSize; return this.displayedEpics.length < this.bufferSize;
}, },
...@@ -90,7 +101,7 @@ export default { ...@@ -90,7 +101,7 @@ export default {
window.removeEventListener('resize', this.syncClientWidth); window.removeEventListener('resize', this.syncClientWidth);
}, },
methods: { methods: {
...mapActions(['setBufferSize', 'toggleEpic']), ...mapActions(['setBufferSize', 'toggleEpic', 'fetchEpics']),
initMounted() { initMounted() {
this.roadmapShellEl = this.$root.$el && this.$root.$el.querySelector('.js-roadmap-shell'); this.roadmapShellEl = this.$root.$el && this.$root.$el.querySelector('.js-roadmap-shell');
this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT)); this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
...@@ -138,6 +149,12 @@ export default { ...@@ -138,6 +149,12 @@ export default {
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) { handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight; this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
}, },
handleScrolledToEnd() {
const { hasNextPage, endCursor } = this.pageInfo;
if (!this.epicsFetchForNextPageInProgress && hasNextPage) {
this.fetchEpics({ endCursor });
}
},
toggleIsEpicExpanded(epic) { toggleIsEpicExpanded(epic) {
this.toggleEpic({ parentItem: epic }); this.toggleEpic({ parentItem: epic });
}, },
...@@ -172,6 +189,16 @@ export default { ...@@ -172,6 +189,16 @@ export default {
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
</span> </span>
</div> </div>
<gl-intersection-observer v-if="glFeatures.performanceRoadmap" @appear="handleScrolledToEnd">
<div
v-if="epicsFetchForNextPageInProgress"
class="gl-text-center gl-py-3"
data-testid="next-page-loading"
>
<gl-loading-icon inline class="gl-mr-2" />
{{ s__('GroupRoadmap|Loading epics') }}
</div>
</gl-intersection-observer>
<div <div
v-show="showBottomShadow" v-show="showBottomShadow"
:style="shadowCellStyles" :style="shadowCellStyles"
......
...@@ -74,3 +74,5 @@ export const EPIC_LEVEL_MARGIN = { ...@@ -74,3 +74,5 @@ export const EPIC_LEVEL_MARGIN = {
export const EPICS_LIMIT_DISMISSED_COOKIE_NAME = 'epics_limit_warning_dismissed'; export const EPICS_LIMIT_DISMISSED_COOKIE_NAME = 'epics_limit_warning_dismissed';
export const EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT = 365; export const EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT = 365;
export const ROADMAP_PAGE_SIZE = gon.features?.performanceRoadmap ? 50 : gon.roadmap_epics_limit;
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./epic.fragment.graphql" #import "./epic.fragment.graphql"
query groupEpics( query groupEpics(
...@@ -12,8 +13,9 @@ query groupEpics( ...@@ -12,8 +13,9 @@ query groupEpics(
$myReactionEmoji: String $myReactionEmoji: String
$confidential: Boolean $confidential: Boolean
$search: String = "" $search: String = ""
$first: Int = 1001 $first: Int = 50
$not: NegatedEpicFilterInput $not: NegatedEpicFilterInput
$endCursor: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
id id
...@@ -31,6 +33,7 @@ query groupEpics( ...@@ -31,6 +33,7 @@ query groupEpics(
first: $first first: $first
timeframe: $timeframe timeframe: $timeframe
not: $not not: $not
after: $endCursor
) { ) {
edges { edges {
node { node {
...@@ -40,6 +43,9 @@ query groupEpics( ...@@ -40,6 +43,9 @@ query groupEpics(
} }
} }
} }
pageInfo {
...PageInfo
}
} }
} }
} }
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { EXTEND_AS } from '../constants'; import { EXTEND_AS, ROADMAP_PAGE_SIZE } from '../constants';
import epicChildEpics from '../queries/epicChildEpics.query.graphql'; import epicChildEpics from '../queries/epicChildEpics.query.graphql';
import groupEpics from '../queries/groupEpics.query.graphql'; import groupEpics from '../queries/groupEpics.query.graphql';
import groupMilestones from '../queries/groupMilestones.query.graphql'; import groupMilestones from '../queries/groupMilestones.query.graphql';
...@@ -19,13 +19,14 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT ...@@ -19,13 +19,14 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT
const fetchGroupEpics = ( const fetchGroupEpics = (
{ epicIid, fullPath, epicsState, sortedBy, presetType, filterParams, timeframe }, { epicIid, fullPath, epicsState, sortedBy, presetType, filterParams, timeframe },
defaultTimeframe, { timeframe: defaultTimeframe, endCursor },
) => { ) => {
let query; let query;
let variables = { let variables = {
fullPath, fullPath,
state: epicsState, state: epicsState,
sort: sortedBy, sort: sortedBy,
endCursor,
...getEpicsTimeframeRange({ ...getEpicsTimeframeRange({
presetType, presetType,
timeframe: defaultTimeframe || timeframe, timeframe: defaultTimeframe || timeframe,
...@@ -45,7 +46,7 @@ const fetchGroupEpics = ( ...@@ -45,7 +46,7 @@ const fetchGroupEpics = (
variables = { variables = {
...variables, ...variables,
...transformedFilterParams, ...transformedFilterParams,
first: gon.roadmap_epics_limit + 1, first: ROADMAP_PAGE_SIZE,
}; };
if (transformedFilterParams?.epicIid) { if (transformedFilterParams?.epicIid) {
...@@ -63,7 +64,10 @@ const fetchGroupEpics = ( ...@@ -63,7 +64,10 @@ const fetchGroupEpics = (
? data?.group?.epic?.children?.edges || [] ? data?.group?.epic?.children?.edges || []
: data?.group?.epics?.edges || []; : data?.group?.epics?.edges || [];
return edges.map((e) => e.node); return {
rawEpics: edges.map((e) => e.node),
pageInfo: data?.group?.epics?.pageInfo,
};
}); });
}; };
...@@ -84,7 +88,7 @@ export const fetchChildrenEpics = (state, { parentItem }) => { ...@@ -84,7 +88,7 @@ export const fetchChildrenEpics = (state, { parentItem }) => {
export const receiveEpicsSuccess = ( export const receiveEpicsSuccess = (
{ commit, dispatch, state }, { commit, dispatch, state },
{ rawEpics, newEpic, timeframeExtended }, { rawEpics, pageInfo, newEpic, timeframeExtended, appendToList },
) => { ) => {
const epicIds = []; const epicIds = [];
const epics = rawEpics.reduce((filteredEpics, epic) => { const epics = rawEpics.reduce((filteredEpics, epic) => {
...@@ -119,8 +123,11 @@ export const receiveEpicsSuccess = ( ...@@ -119,8 +123,11 @@ export const receiveEpicsSuccess = (
const updatedEpics = state.epics.concat(epics); const updatedEpics = state.epics.concat(epics);
sortEpics(updatedEpics, state.sortedBy); sortEpics(updatedEpics, state.sortedBy);
commit(types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS, updatedEpics); commit(types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS, updatedEpics);
} else if (appendToList) {
const updatedEpics = state.epics.concat(epics);
commit(types.RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS, { epics: updatedEpics, pageInfo });
} else { } else {
commit(types.RECEIVE_EPICS_SUCCESS, epics); commit(types.RECEIVE_EPICS_SUCCESS, { epics, pageInfo });
} }
}; };
export const receiveEpicsFailure = ({ commit }) => { export const receiveEpicsFailure = ({ commit }) => {
...@@ -160,12 +167,20 @@ export const receiveChildrenSuccess = ( ...@@ -160,12 +167,20 @@ export const receiveChildrenSuccess = (
commit(types.RECEIVE_CHILDREN_SUCCESS, { parentItemId, children }); commit(types.RECEIVE_CHILDREN_SUCCESS, { parentItemId, children });
}; };
export const fetchEpics = ({ state, commit, dispatch }) => { export const fetchEpics = ({ state, commit, dispatch }, { endCursor } = {}) => {
commit(types.REQUEST_EPICS); if (endCursor) {
commit(types.REQUEST_EPICS_FOR_NEXT_PAGE);
} else {
commit(types.REQUEST_EPICS);
}
fetchGroupEpics(state) fetchGroupEpics(state, { endCursor })
.then((rawEpics) => { .then(({ rawEpics, pageInfo }) => {
dispatch('receiveEpicsSuccess', { rawEpics }); dispatch('receiveEpicsSuccess', {
rawEpics,
pageInfo,
appendToList: Boolean(endCursor),
});
}) })
.catch(() => dispatch('receiveEpicsFailure')); .catch(() => dispatch('receiveEpicsFailure'));
}; };
...@@ -173,10 +188,11 @@ export const fetchEpics = ({ state, commit, dispatch }) => { ...@@ -173,10 +188,11 @@ export const fetchEpics = ({ state, commit, dispatch }) => {
export const fetchEpicsForTimeframe = ({ state, commit, dispatch }, { timeframe }) => { export const fetchEpicsForTimeframe = ({ state, commit, dispatch }, { timeframe }) => {
commit(types.REQUEST_EPICS_FOR_TIMEFRAME); commit(types.REQUEST_EPICS_FOR_TIMEFRAME);
return fetchGroupEpics(state, timeframe) return fetchGroupEpics(state, { timeframe })
.then((rawEpics) => { .then(({ rawEpics, pageInfo }) => {
dispatch('receiveEpicsSuccess', { dispatch('receiveEpicsSuccess', {
rawEpics, rawEpics,
pageInfo,
newEpic: true, newEpic: true,
timeframeExtended: true, timeframeExtended: true,
}); });
......
...@@ -6,8 +6,10 @@ export const UPDATE_EPIC_IDS = 'UPDATE_EPIC_IDS'; ...@@ -6,8 +6,10 @@ export const UPDATE_EPIC_IDS = 'UPDATE_EPIC_IDS';
export const REQUEST_EPICS = 'REQUEST_EPICS'; export const REQUEST_EPICS = 'REQUEST_EPICS';
export const REQUEST_EPICS_FOR_TIMEFRAME = 'REQUEST_EPICS_FOR_TIMEFRAME'; export const REQUEST_EPICS_FOR_TIMEFRAME = 'REQUEST_EPICS_FOR_TIMEFRAME';
export const REQUEST_EPICS_FOR_NEXT_PAGE = 'REQUEST_EPICS_FOR_NEXT_PAGE';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS'; export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS = 'RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS'; export const RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS = 'RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS';
export const RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS = 'RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS';
export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE'; export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
......
...@@ -27,11 +27,15 @@ export default { ...@@ -27,11 +27,15 @@ export default {
[types.REQUEST_EPICS_FOR_TIMEFRAME](state) { [types.REQUEST_EPICS_FOR_TIMEFRAME](state) {
state.epicsFetchForTimeframeInProgress = true; state.epicsFetchForTimeframeInProgress = true;
}, },
[types.RECEIVE_EPICS_SUCCESS](state, epics) { [types.REQUEST_EPICS_FOR_NEXT_PAGE](state) {
state.epicsFetchForNextPageInProgress = true;
},
[types.RECEIVE_EPICS_SUCCESS](state, { epics, pageInfo }) {
state.epicsFetchResultEmpty = epics.length === 0; state.epicsFetchResultEmpty = epics.length === 0;
if (!state.epicsFetchResultEmpty) { if (!state.epicsFetchResultEmpty) {
state.epics = epics; state.epics = epics;
state.pageInfo = pageInfo;
} }
state.epicsFetchInProgress = false; state.epicsFetchInProgress = false;
...@@ -40,9 +44,15 @@ export default { ...@@ -40,9 +44,15 @@ export default {
state.epics = epics; state.epics = epics;
state.epicsFetchForTimeframeInProgress = false; state.epicsFetchForTimeframeInProgress = false;
}, },
[types.RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS](state, { epics, pageInfo }) {
state.epics = epics;
state.pageInfo = pageInfo;
state.epicsFetchForNextPageInProgress = false;
},
[types.RECEIVE_EPICS_FAILURE](state) { [types.RECEIVE_EPICS_FAILURE](state) {
state.epicsFetchInProgress = false; state.epicsFetchInProgress = false;
state.epicsFetchForTimeframeInProgress = false; state.epicsFetchForTimeframeInProgress = false;
state.epicsFetchForNextPageInProgress = false;
state.epicsFetchFailure = true; state.epicsFetchFailure = true;
Object.keys(state.childrenEpics).forEach((id) => { Object.keys(state.childrenEpics).forEach((id) => {
Vue.set(state.childrenFlags, id, { Vue.set(state.childrenFlags, id, {
......
...@@ -7,6 +7,7 @@ export default () => ({ ...@@ -7,6 +7,7 @@ export default () => ({
// Data // Data
epicIid: '', epicIid: '',
epics: [], epics: [],
pageInfo: null,
childrenEpics: {}, childrenEpics: {},
childrenFlags: {}, childrenFlags: {},
visibleEpics: [], visibleEpics: [],
...@@ -27,6 +28,7 @@ export default () => ({ ...@@ -27,6 +28,7 @@ export default () => ({
hasFiltersApplied: false, hasFiltersApplied: false,
epicsFetchInProgress: false, epicsFetchInProgress: false,
epicsFetchForTimeframeInProgress: false, epicsFetchForTimeframeInProgress: false,
epicsFetchForNextPageInProgress: false,
epicsFetchFailure: false, epicsFetchFailure: false,
epicsFetchResultEmpty: false, epicsFetchResultEmpty: false,
milestonesFetchInProgress: false, milestonesFetchInProgress: false,
......
...@@ -9,6 +9,7 @@ module Groups ...@@ -9,6 +9,7 @@ module Groups
before_action :persist_roadmap_layout, only: [:show] before_action :persist_roadmap_layout, only: [:show]
before_action do before_action do
push_frontend_feature_flag(:async_filtering, @group, default_enabled: true) push_frontend_feature_flag(:async_filtering, @group, default_enabled: true)
push_frontend_feature_flag(:performance_roadmap, @group, default_enabled: :yaml)
end end
feature_category :roadmaps feature_category :roadmaps
......
---
name: performance_roadmap
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65652
rollout_issue_url:
milestone: '14.2'
type: development
group: group::product planning
default_enabled: false
...@@ -28,6 +28,7 @@ RSpec.describe 'group epic roadmap', :js do ...@@ -28,6 +28,7 @@ RSpec.describe 'group epic roadmap', :js do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
stub_feature_flags(unfiltered_epic_aggregates: false) stub_feature_flags(unfiltered_epic_aggregates: false)
stub_feature_flags(async_filtering: false) stub_feature_flags(async_filtering: false)
stub_feature_flags(performance_roadmap: false)
sign_in(user) sign_in(user)
end end
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import EpicItem from 'ee/roadmap/components/epic_item.vue'; import EpicItem from 'ee/roadmap/components/epic_item.vue';
import EpicsListSection from 'ee/roadmap/components/epics_list_section.vue'; import EpicsListSection from 'ee/roadmap/components/epics_list_section.vue';
import { import {
...@@ -7,6 +9,7 @@ import { ...@@ -7,6 +9,7 @@ import {
TIMELINE_CELL_MIN_WIDTH, TIMELINE_CELL_MIN_WIDTH,
} from 'ee/roadmap/constants'; } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
import { REQUEST_EPICS_FOR_NEXT_PAGE } from 'ee/roadmap/store/mutation_types';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { import {
mockFormattedChildEpic1, mockFormattedChildEpic1,
...@@ -16,8 +19,10 @@ import { ...@@ -16,8 +19,10 @@ import {
rawEpics, rawEpics,
mockEpicsWithParents, mockEpicsWithParents,
mockSortedBy, mockSortedBy,
mockPageInfo,
basePath, basePath,
} from 'ee_jest/roadmap/mock_data'; } from 'ee_jest/roadmap/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = createStore(); const store = createStore();
...@@ -30,7 +35,11 @@ store.dispatch('setInitialData', { ...@@ -30,7 +35,11 @@ store.dispatch('setInitialData', {
basePath, basePath,
}); });
store.dispatch('receiveEpicsSuccess', { rawEpics }); store.dispatch('receiveEpicsSuccess', {
rawEpics,
pageInfo: mockPageInfo,
appendToList: true,
});
const mockEpics = store.state.epics; const mockEpics = store.state.epics;
...@@ -44,8 +53,9 @@ const createComponent = ({ ...@@ -44,8 +53,9 @@ const createComponent = ({
currentGroupId = mockGroupId, currentGroupId = mockGroupId,
presetType = PRESET_TYPES.MONTHS, presetType = PRESET_TYPES.MONTHS,
hasFiltersApplied = false, hasFiltersApplied = false,
performanceRoadmap = false,
} = {}) => { } = {}) => {
return shallowMount(EpicsListSection, { return shallowMountExtended(EpicsListSection, {
localVue, localVue,
store, store,
stubs: { stubs: {
...@@ -59,6 +69,11 @@ const createComponent = ({ ...@@ -59,6 +69,11 @@ const createComponent = ({
currentGroupId, currentGroupId,
hasFiltersApplied, hasFiltersApplied,
}, },
provide: {
glFeatures: {
performanceRoadmap,
},
},
}); });
}; };
...@@ -66,6 +81,7 @@ describe('EpicsListSectionComponent', () => { ...@@ -66,6 +81,7 @@ describe('EpicsListSectionComponent', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
gon.features = { performanceRoadmap: false };
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -253,6 +269,38 @@ describe('EpicsListSectionComponent', () => { ...@@ -253,6 +269,38 @@ describe('EpicsListSectionComponent', () => {
expect(wrapper.find('.epic-scroll-bottom-shadow').exists()).toBe(true); expect(wrapper.find('.epic-scroll-bottom-shadow').exists()).toBe(true);
}); });
describe('when `performanceRoadmap` feature flag is enabled', () => {
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
beforeEach(() => {
gon.features = { performanceRoadmap: true };
wrapper = createComponent({ performanceRoadmap: true });
});
it('renders gl-intersection-observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
it('calls action `fetchEpics` when gl-intersection-observer appears in viewport', () => {
const fakeFetchEpics = jest.spyOn(wrapper.vm, 'fetchEpics').mockImplementation();
findIntersectionObserver().vm.$emit('appear');
expect(fakeFetchEpics).toHaveBeenCalledWith({
endCursor: mockPageInfo.endCursor,
});
});
it('renders gl-loading icon when epicsFetchForNextPageInProgress is true', async () => {
wrapper.vm.$store.commit(REQUEST_EPICS_FOR_NEXT_PAGE);
await wrapper.vm.$nextTick();
expect(wrapper.findByTestId('next-page-loading').text()).toContain('Loading epics');
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
}); });
it('expands to show child epics when epic is toggled', () => { it('expands to show child epics when epic is toggled', () => {
......
...@@ -82,7 +82,7 @@ describe('RoadmapApp', () => { ...@@ -82,7 +82,7 @@ describe('RoadmapApp', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
if (epicList) { if (epicList) {
store.commit(types.RECEIVE_EPICS_SUCCESS, epicList); store.commit(types.RECEIVE_EPICS_SUCCESS, { epics: epicList });
} }
}); });
...@@ -103,7 +103,7 @@ describe('RoadmapApp', () => { ...@@ -103,7 +103,7 @@ describe('RoadmapApp', () => {
describe('empty state view', () => { describe('empty state view', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
store.commit(types.RECEIVE_EPICS_SUCCESS, []); store.commit(types.RECEIVE_EPICS_SUCCESS, { epics: [] });
}); });
it('contains path for the empty state illustration', () => { it('contains path for the empty state illustration', () => {
...@@ -138,7 +138,7 @@ describe('RoadmapApp', () => { ...@@ -138,7 +138,7 @@ describe('RoadmapApp', () => {
describe('roadmap view', () => { describe('roadmap view', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
store.commit(types.RECEIVE_EPICS_SUCCESS, epics); store.commit(types.RECEIVE_EPICS_SUCCESS, { epics });
}); });
it('contains roadmap filters UI', () => { it('contains roadmap filters UI', () => {
...@@ -239,7 +239,9 @@ describe('RoadmapApp', () => { ...@@ -239,7 +239,9 @@ describe('RoadmapApp', () => {
describe('roadmap epics limit warning', () => { describe('roadmap epics limit warning', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
store.commit(types.RECEIVE_EPICS_SUCCESS, [mockFormattedEpic, mockFormattedChildEpic2]); store.commit(types.RECEIVE_EPICS_SUCCESS, {
epics: [mockFormattedEpic, mockFormattedChildEpic2],
});
window.gon.roadmap_epics_limit = 1; window.gon.roadmap_epics_limit = 1;
}); });
......
...@@ -548,6 +548,14 @@ export const mockEpicNode2 = { ...@@ -548,6 +548,14 @@ export const mockEpicNode2 = {
export const mockGroupEpics = [mockEpicNode1, mockEpicNode2]; export const mockGroupEpics = [mockEpicNode1, mockEpicNode2];
export const mockPageInfo = {
endCursor: 'eyJzdGFydF9kYXRlIjoiMjAyMC0wOS0wMSIsImlkIjoiMzExIn0',
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'eyJzdGFydF9kYXRlIjoiMjAyMC0wNC0xOCIsImlkIjoiMjQ1In0',
__typename: 'PageInfo',
};
export const mockGroupEpicsQueryResponse = { export const mockGroupEpicsQueryResponse = {
data: { data: {
group: { group: {
...@@ -568,6 +576,9 @@ export const mockGroupEpicsQueryResponse = { ...@@ -568,6 +576,9 @@ export const mockGroupEpicsQueryResponse = {
__typename: 'EpicEdge', __typename: 'EpicEdge',
}, },
], ],
pageInfo: {
...mockPageInfo,
},
__typename: 'EpicConnection', __typename: 'EpicConnection',
}, },
__typename: 'Group', __typename: 'Group',
......
...@@ -30,6 +30,7 @@ import { ...@@ -30,6 +30,7 @@ import {
mockGroupMilestones, mockGroupMilestones,
mockMilestone, mockMilestone,
mockFormattedMilestone, mockFormattedMilestone,
mockPageInfo,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -78,13 +79,17 @@ describe('Roadmap Vuex Actions', () => { ...@@ -78,13 +79,17 @@ describe('Roadmap Vuex Actions', () => {
actions.receiveEpicsSuccess, actions.receiveEpicsSuccess,
{ {
rawEpics: [mockRawEpic2], rawEpics: [mockRawEpic2],
pageInfo: mockPageInfo,
}, },
state, state,
[ [
{ type: types.UPDATE_EPIC_IDS, payload: [mockRawEpic2.id] }, {
type: types.UPDATE_EPIC_IDS,
payload: [mockRawEpic2.id],
},
{ {
type: types.RECEIVE_EPICS_SUCCESS, type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockFormattedEpic2], payload: { epics: [mockFormattedEpic2], pageInfo: mockPageInfo },
}, },
], ],
[ [
...@@ -174,7 +179,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -174,7 +179,7 @@ describe('Roadmap Vuex Actions', () => {
return testAction( return testAction(
actions.fetchEpics, actions.fetchEpics,
null, {},
state, state,
[ [
{ {
...@@ -184,7 +189,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -184,7 +189,7 @@ describe('Roadmap Vuex Actions', () => {
[ [
{ {
type: 'receiveEpicsSuccess', type: 'receiveEpicsSuccess',
payload: { rawEpics: mockGroupEpics }, payload: { rawEpics: mockGroupEpics, pageInfo: mockPageInfo, appendToList: false },
}, },
], ],
); );
...@@ -197,7 +202,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -197,7 +202,7 @@ describe('Roadmap Vuex Actions', () => {
return testAction( return testAction(
actions.fetchEpics, actions.fetchEpics,
null, {},
state, state,
[ [
{ {
...@@ -237,6 +242,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -237,6 +242,7 @@ describe('Roadmap Vuex Actions', () => {
type: 'receiveEpicsSuccess', type: 'receiveEpicsSuccess',
payload: { payload: {
rawEpics: mockGroupEpics, rawEpics: mockGroupEpics,
pageInfo: mockPageInfo,
newEpic: true, newEpic: true,
timeframeExtended: true, timeframeExtended: true,
}, },
......
...@@ -3,7 +3,13 @@ import mutations from 'ee/roadmap/store/mutations'; ...@@ -3,7 +3,13 @@ import mutations from 'ee/roadmap/store/mutations';
import defaultState from 'ee/roadmap/store/state'; import defaultState from 'ee/roadmap/store/state';
import { mockGroupId, basePath, mockSortedBy, mockEpic } from 'ee_jest/roadmap/mock_data'; import {
mockGroupId,
basePath,
mockSortedBy,
mockEpic,
mockPageInfo,
} from 'ee_jest/roadmap/mock_data';
const setEpicMockData = (state) => { const setEpicMockData = (state) => {
state.epics = [mockEpic]; state.epics = [mockEpic];
...@@ -77,14 +83,23 @@ describe('Roadmap Store Mutations', () => { ...@@ -77,14 +83,23 @@ describe('Roadmap Store Mutations', () => {
}); });
}); });
describe('REQUEST_EPICS_FOR_NEXT_PAGE', () => {
it('Should set state.epicsFetchForNextPageInProgress to `true`', () => {
mutations[types.REQUEST_EPICS_FOR_NEXT_PAGE](state);
expect(state.epicsFetchForNextPageInProgress).toBe(true);
});
});
describe('RECEIVE_EPICS_SUCCESS', () => { describe('RECEIVE_EPICS_SUCCESS', () => {
it('Should set epicsFetchResultEmpty, epics in state based on provided epics array and set epicsFetchInProgress to `false`', () => { it('Should set epicsFetchResultEmpty, epics in state based on provided epics array and set epicsFetchInProgress to `false`', () => {
const epics = [{ id: 1 }, { id: 2 }]; const epics = [{ id: 1 }, { id: 2 }];
mutations[types.RECEIVE_EPICS_SUCCESS](state, epics); mutations[types.RECEIVE_EPICS_SUCCESS](state, { epics, pageInfo: mockPageInfo });
expect(state.epicsFetchResultEmpty).toBe(false); expect(state.epicsFetchResultEmpty).toBe(false);
expect(state.epics).toEqual(epics); expect(state.epics).toEqual(epics);
expect(state.pageInfo).toEqual(mockPageInfo);
expect(state.epicsFetchInProgress).toBe(false); expect(state.epicsFetchInProgress).toBe(false);
}); });
}); });
...@@ -100,12 +115,28 @@ describe('Roadmap Store Mutations', () => { ...@@ -100,12 +115,28 @@ describe('Roadmap Store Mutations', () => {
}); });
}); });
describe('RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS', () => {
it('Should set epics in state based on provided epics array and set epicsFetchForNextPageInProgress to `false`', () => {
const epics = [{ id: 1 }, { id: 2 }];
mutations[types.RECEIVE_EPICS_FOR_NEXT_PAGE_SUCCESS](state, {
epics,
pageInfo: mockPageInfo,
});
expect(state.epics).toEqual(epics);
expect(state.pageInfo).toEqual(mockPageInfo);
expect(state.epicsFetchForNextPageInProgress).toBe(false);
});
});
describe('RECEIVE_EPICS_FAILURE', () => { describe('RECEIVE_EPICS_FAILURE', () => {
it('Should set epicsFetchInProgress & epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', () => { it('Should set epicsFetchInProgress & epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', () => {
mutations[types.RECEIVE_EPICS_FAILURE](state); mutations[types.RECEIVE_EPICS_FAILURE](state);
expect(state.epicsFetchInProgress).toBe(false); expect(state.epicsFetchInProgress).toBe(false);
expect(state.epicsFetchForTimeframeInProgress).toBe(false); expect(state.epicsFetchForTimeframeInProgress).toBe(false);
expect(state.epicsFetchForNextPageInProgress).toBe(false);
expect(state.epicsFetchFailure).toBe(true); expect(state.epicsFetchFailure).toBe(true);
}); });
}); });
......
...@@ -15638,6 +15638,9 @@ msgstr "" ...@@ -15638,6 +15638,9 @@ msgstr ""
msgid "GroupRoadmap|%{startDateInWords} – %{endDateInWords}" msgid "GroupRoadmap|%{startDateInWords} – %{endDateInWords}"
msgstr "" msgstr ""
msgid "GroupRoadmap|Loading epics"
msgstr ""
msgid "GroupRoadmap|No start and end date" msgid "GroupRoadmap|No start and end date"
msgstr "" msgstr ""
......
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