Commit b9051ffe authored by Phil Hughes's avatar Phil Hughes

Merge branch '299814-add-roadmap-pagination' into 'master'

Use pagination to load epics in roadmap

See merge request gitlab-org/gitlab!65652
parents 5912f3de 9c0b873f
<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 } = {}) => {
if (endCursor) {
commit(types.REQUEST_EPICS_FOR_NEXT_PAGE);
} else {
commit(types.REQUEST_EPICS); 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