Commit edaa8a5f authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '9831-roadmap-vuex-epics-fetch' into 'master'

[Part 2] Fetch epics for Roadmap via Vuex

Closes #9831

See merge request gitlab-org/gitlab-ee!10419
parents 4229e13f 7a42f45d
<script> <script>
import { mapState, mapActions } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Flash from '~/flash';
import { s__ } from '~/locale';
import epicsListEmpty from './epics_list_empty.vue'; import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue'; import roadmapShell from './roadmap_shell.vue';
...@@ -15,14 +14,6 @@ export default { ...@@ -15,14 +14,6 @@ export default {
roadmapShell, roadmapShell,
}, },
props: { props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
presetType: { presetType: {
type: String, type: String,
required: true, required: true,
...@@ -42,19 +33,20 @@ export default { ...@@ -42,19 +33,20 @@ export default {
}, },
data() { data() {
return { return {
isLoading: false,
isEpicsListEmpty: false,
hasError: false,
handleResizeThrottled: {}, handleResizeThrottled: {},
}; };
}, },
computed: { computed: {
epics() { ...mapState([
return this.store.getEpics(); 'currentGroupId',
}, 'epics',
timeframe() { 'timeframe',
return this.store.getTimeframe(); 'extendedTimeframe',
}, 'epicsFetchInProgress',
'epicsFetchForTimeframeInProgress',
'epicsFetchResultEmpty',
'epicsFetchFailure',
]),
timeframeStart() { timeframeStart() {
return this.timeframe[0]; return this.timeframe[0];
}, },
...@@ -62,71 +54,32 @@ export default { ...@@ -62,71 +54,32 @@ export default {
const last = this.timeframe.length - 1; const last = this.timeframe.length - 1;
return this.timeframe[last]; return this.timeframe[last];
}, },
currentGroupId() {
return this.store.getCurrentGroupId();
},
showRoadmap() { showRoadmap() {
return !this.hasError && !this.isLoading && !this.isEpicsListEmpty; return !this.epicsFetchFailure && !this.epicsFetchInProgress && !this.epicsFetchResultEmpty;
},
}, },
mounted() {
this.fetchEpics();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
}, },
beforeDestroy() { watch: {
window.removeEventListener('resize', this.handleResizeThrottled, false); epicsFetchInProgress(value) {
}, if (!value && this.epics.length) {
methods: {
fetchEpics() {
this.hasError = false;
this.service
.getEpics()
.then(res => res.data)
.then(epics => {
if (epics.length) {
this.store.setEpics(epics);
this.$nextTick(() => { this.$nextTick(() => {
// Render timeline bars as we're already having timeline
// rendered before fetch
eventHub.$emit('refreshTimeline', { eventHub.$emit('refreshTimeline', {
todayBarReady: true, todayBarReady: true,
initialRender: true, initialRender: true,
}); });
}); });
} else {
this.isEpicsListEmpty = true;
} }
})
.catch(() => {
this.isLoading = false;
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
}, },
fetchEpicsForTimeframe({ timeframe, roadmapTimelineEl, extendType }) {
this.hasError = false;
this.service
.getEpicsForTimeframe(this.presetType, timeframe)
.then(res => res.data)
.then(epics => {
if (epics.length) {
this.store.addEpics(epics);
}
this.$nextTick(() => {
// Re-render timeline bars with updated timeline
this.processExtendedTimeline({
itemsCount: timeframe ? timeframe.length : 0,
extendType,
roadmapTimelineEl,
});
});
})
.catch(() => {
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
}, },
mounted() {
this.fetchEpics();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
},
methods: {
...mapActions(['fetchEpics', 'fetchEpicsForTimeframe', 'extendTimeframe', 'refreshEpicDates']),
/** /**
* Roadmap view works with absolute sizing and positioning * Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell; * of following child components of RoadmapShell;
...@@ -175,21 +128,32 @@ export default { ...@@ -175,21 +128,32 @@ export default {
} }
}, },
handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) { handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) {
const timeframe = this.store.extendTimeframe(extendType); this.extendTimeframe({ extendAs: extendType });
this.refreshEpicDates();
this.$nextTick(() => { this.$nextTick(() => {
this.fetchEpicsForTimeframe({ this.fetchEpicsForTimeframe({
timeframe, timeframe: this.extendedTimeframe,
})
.then(() => {
this.$nextTick(() => {
// Re-render timeline bars with updated timeline
this.processExtendedTimeline({
itemsCount: this.extendedTimeframe ? this.extendedTimeframe.length : 0,
extendType, extendType,
roadmapTimelineEl, roadmapTimelineEl,
}); });
}); });
})
.catch(() => {});
});
}, },
}, },
}; };
</script> </script>
<template> <template>
<div :class="{ 'overflow-reset': isEpicsListEmpty }" class="roadmap-container"> <div :class="{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container">
<roadmap-shell <roadmap-shell
v-if="showRoadmap" v-if="showRoadmap"
:preset-type="presetType" :preset-type="presetType"
...@@ -200,7 +164,7 @@ export default { ...@@ -200,7 +164,7 @@ export default {
@onScrollToEnd="handleScrollToExtend" @onScrollToEnd="handleScrollToExtend"
/> />
<epics-list-empty <epics-list-empty
v-if="isEpicsListEmpty" v-if="epicsFetchResultEmpty"
:preset-type="presetType" :preset-type="presetType"
:timeframe-start="timeframeStart" :timeframe-start="timeframeStart"
:timeframe-end="timeframeEnd" :timeframe-end="timeframeEnd"
......
...@@ -11,8 +11,6 @@ import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants'; ...@@ -11,8 +11,6 @@ import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants';
import { getTimeframeForPreset, getEpicsPathForPreset } from './utils/roadmap_utils'; import { getTimeframeForPreset, getEpicsPathForPreset } from './utils/roadmap_utils';
import createStore from './store'; import createStore from './store';
import RoadmapStore from './store/roadmap_store';
import RoadmapService from './service/roadmap_service';
import roadmapApp from './components/app.vue'; import roadmapApp from './components/app.vue';
...@@ -45,7 +43,6 @@ export default () => { ...@@ -45,7 +43,6 @@ export default () => {
data() { data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES); const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
const hasFiltersApplied = parseBoolean(dataset.hasFiltersApplied);
const presetType = const presetType =
supportedPresetTypes.indexOf(dataset.presetType) > -1 supportedPresetTypes.indexOf(dataset.presetType) > -1
? dataset.presetType ? dataset.presetType
...@@ -63,32 +60,17 @@ export default () => { ...@@ -63,32 +60,17 @@ export default () => {
timeframe, timeframe,
}); });
const store = new RoadmapStore({
groupId: parseInt(dataset.groupId, 0),
sortedBy: dataset.sortedBy,
timeframe,
presetType,
});
const service = new RoadmapService({
initialEpicsPath,
filterQueryString,
basePath: dataset.epicsPath,
epicsState: dataset.epicsState,
});
return { return {
store,
service,
presetType,
hasFiltersApplied,
epicsState: dataset.epicsState,
newEpicEndpoint: dataset.newEpicEndpoint,
emptyStateIllustrationPath: dataset.emptyStateIllustration, emptyStateIllustrationPath: dataset.emptyStateIllustration,
hasFiltersApplied: parseBoolean(dataset.hasFiltersApplied),
// Part of Vuex Store
currentGroupId: parseInt(dataset.groupId, 0), currentGroupId: parseInt(dataset.groupId, 0),
newEpicEndpoint: dataset.newEpicEndpoint,
epicsState: dataset.epicsState,
basePath: dataset.epicsPath,
sortedBy: dataset.sortedBy, sortedBy: dataset.sortedBy,
filterQueryString,
initialEpicsPath,
presetType,
timeframe, timeframe,
}; };
}, },
...@@ -98,6 +80,9 @@ export default () => { ...@@ -98,6 +80,9 @@ export default () => {
sortedBy: this.sortedBy, sortedBy: this.sortedBy,
presetType: this.presetType, presetType: this.presetType,
timeframe: this.timeframe, timeframe: this.timeframe,
basePath: this.basePath,
filterQueryString: this.filterQueryString,
initialEpicsPath: this.initialEpicsPath,
}); });
}, },
methods: { methods: {
...@@ -107,7 +92,6 @@ export default () => { ...@@ -107,7 +92,6 @@ export default () => {
return createElement('roadmap-app', { return createElement('roadmap-app', {
props: { props: {
store: this.store, store: this.store,
service: this.service,
presetType: this.presetType, presetType: this.presetType,
hasFiltersApplied: this.hasFiltersApplied, hasFiltersApplied: this.hasFiltersApplied,
epicsState: this.epicsState, epicsState: this.epicsState,
......
import axios from '~/lib/utils/axios_utils';
import { getEpicsPathForPreset } from '../utils/roadmap_utils';
export default class RoadmapService {
constructor({ basePath, epicsState, filterQueryString, initialEpicsPath }) {
this.basePath = basePath;
this.epicsState = epicsState;
this.filterQueryString = filterQueryString;
this.initialEpicsPath = initialEpicsPath;
}
getEpics() {
return axios.get(this.initialEpicsPath);
}
getEpicsForTimeframe(presetType, timeframe) {
const epicsPath = getEpicsPathForPreset({
basePath: this.basePath,
epicsState: this.epicsState,
filterQueryString: this.filterQueryString,
presetType,
timeframe,
});
return axios.get(epicsPath);
}
}
import flash from '~/flash';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import * as epicUtils from '../utils/epic_utils';
import { getEpicsPathForPreset, sortEpics, extendTimeframeForPreset } from '../utils/roadmap_utils';
import { EXTEND_AS } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const requestEpicsForTimeframe = ({ commit }) => commit(types.REQUEST_EPICS_FOR_TIMEFRAME);
export const receiveEpicsSuccess = (
{ commit, state, getters },
{ rawEpics, newEpic, timeframeExtended },
) => {
const epics = rawEpics.reduce((filteredEpics, epic) => {
const formattedEpic = epicUtils.formatEpicDetails(
epic,
getters.timeframeStartDate,
getters.timeframeEndDate,
);
// Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline
if (
formattedEpic.startDate <= formattedEpic.endDate &&
state.epicIds.indexOf(formattedEpic.id) < 0
) {
Object.assign(formattedEpic, {
newEpic,
});
filteredEpics.push(formattedEpic);
commit(types.UPDATE_EPIC_IDS, formattedEpic.id);
}
return filteredEpics;
}, []);
if (timeframeExtended) {
const updatedEpics = state.epics.concat(epics);
sortEpics(updatedEpics, state.sortedBy);
commit(types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS, updatedEpics);
} else {
commit(types.RECEIVE_EPICS_SUCCESS, epics);
}
};
export const receiveEpicsFailure = ({ commit }) => {
commit(types.RECEIVE_EPICS_FAILURE);
flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
};
export const fetchEpics = ({ state, dispatch }) => {
dispatch('requestEpics');
return axios
.get(state.initialEpicsPath)
.then(({ data }) => {
dispatch('receiveEpicsSuccess', { rawEpics: data });
})
.catch(() => {
dispatch('receiveEpicsFailure');
});
};
export const fetchEpicsForTimeframe = ({ state, dispatch }, { timeframe }) => {
dispatch('requestEpicsForTimeframe');
const epicsPath = getEpicsPathForPreset({
basePath: state.basePath,
epicsState: state.epicsState,
filterQueryString: state.filterQueryString,
presetType: state.presetType,
timeframe,
});
return axios
.get(epicsPath)
.then(({ data }) => {
dispatch('receiveEpicsSuccess', {
rawEpics: data,
newEpic: true,
timeframeExtended: true,
});
})
.catch(() => {
dispatch('receiveEpicsFailure');
});
};
export const extendTimeframe = ({ commit, state, getters }, { extendAs }) => {
const isExtendTypePrepend = extendAs === EXTEND_AS.PREPEND;
const timeframeToExtend = extendTimeframeForPreset({
extendAs,
presetType: state.presetType,
initialDate: isExtendTypePrepend ? getters.timeframeStartDate : getters.timeframeEndDate,
});
if (isExtendTypePrepend) {
commit(types.PREPEND_TIMEFRAME, timeframeToExtend);
} else {
commit(types.APPEND_TIMEFRAME, timeframeToExtend);
}
};
export const refreshEpicDates = ({ commit, state, getters }) => {
const epics = state.epics.map(epic =>
epicUtils.processEpicDates(epic, getters.timeframeStartDate, getters.timeframeEndDate),
);
commit(types.SET_EPICS, epics);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const SET_EPICS = 'SET_EPICS';
export const UPDATE_EPIC_IDS = 'UPDATE_EPIC_IDS';
export const REQUEST_EPICS = 'REQUEST_EPICS'; export const REQUEST_EPICS = 'REQUEST_EPICS';
export const REQUEST_EPICS_SUCCESS = 'REQUEST_EPICS_SUCCESS'; export const REQUEST_EPICS_FOR_TIMEFRAME = 'REQUEST_EPICS_FOR_TIMEFRAME';
export const REQUEST_EPICS_FAILURE = 'REQUEST_EPICS_FAILURE'; export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS = 'RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS';
export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const PREPEND_TIMEFRAME = 'PREPEND_TIMEFRAME';
export const APPEND_TIMEFRAME = 'APPEND_TIMEFRAME';
...@@ -4,4 +4,46 @@ export default { ...@@ -4,4 +4,46 @@ export default {
[types.SET_INITIAL_DATA](state, data) { [types.SET_INITIAL_DATA](state, data) {
Object.assign(state, { ...data }); Object.assign(state, { ...data });
}, },
[types.SET_EPICS](state, epics) {
state.epics = epics;
},
[types.UPDATE_EPIC_IDS](state, epicId) {
state.epicIds.push(epicId);
},
[types.REQUEST_EPICS](state) {
state.epicsFetchInProgress = true;
},
[types.REQUEST_EPICS_FOR_TIMEFRAME](state) {
state.epicsFetchForTimeframeInProgress = true;
},
[types.RECEIVE_EPICS_SUCCESS](state, epics) {
state.epicsFetchResultEmpty = epics.length === 0;
if (!state.epicsFetchResultEmpty) {
state.epics = epics;
}
state.epicsFetchInProgress = false;
},
[types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS](state, epics) {
state.epics = epics;
state.epicsFetchForTimeframeInProgress = false;
},
[types.RECEIVE_EPICS_FAILURE](state) {
state.epicsFetchInProgress = false;
state.epicsFetchForTimeframeInProgress = false;
state.epicsFetchFailure = true;
},
[types.PREPEND_TIMEFRAME](state, extendedTimeframe) {
state.extendedTimeframe = extendedTimeframe;
state.timeframe.unshift(...extendedTimeframe);
},
[types.APPEND_TIMEFRAME](state, extendedTimeframe) {
state.extendedTimeframe = extendedTimeframe;
state.timeframe.push(...extendedTimeframe);
},
}; };
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { extendTimeframeForPreset, sortEpics } from '../utils/roadmap_utils';
import { PRESET_TYPES, EXTEND_AS } from '../constants';
export default class RoadmapStore {
constructor({ groupId, timeframe, presetType, sortedBy }) {
this.state = {};
this.state.epics = [];
this.state.epicIds = [];
this.state.currentGroupId = groupId;
this.state.timeframe = timeframe;
this.presetType = presetType;
this.sortedBy = sortedBy;
this.initTimeframeThreshold();
}
initTimeframeThreshold() {
const [startFrame] = this.state.timeframe;
const lastTimeframeIndex = this.state.timeframe.length - 1;
if (this.presetType === PRESET_TYPES.QUARTERS) {
[this.timeframeStartDate] = startFrame.range;
// eslint-disable-next-line prefer-destructuring
this.timeframeEndDate = this.state.timeframe[lastTimeframeIndex].range[2];
} else if (this.presetType === PRESET_TYPES.MONTHS) {
this.timeframeStartDate = startFrame;
this.timeframeEndDate = this.state.timeframe[lastTimeframeIndex];
} else if (this.presetType === PRESET_TYPES.WEEKS) {
this.timeframeStartDate = startFrame;
this.timeframeEndDate = newDate(this.state.timeframe[lastTimeframeIndex]);
this.timeframeEndDate.setDate(this.timeframeEndDate.getDate() + 7);
}
}
setEpics(epics) {
this.state.epicIds = [];
this.state.epics = RoadmapStore.filterInvalidEpics({
timeframeStartDate: this.timeframeStartDate,
timeframeEndDate: this.timeframeEndDate,
state: this.state,
epics,
});
}
addEpics(epics) {
this.state.epics = this.state.epics.concat(
RoadmapStore.filterInvalidEpics({
timeframeStartDate: this.timeframeStartDate,
timeframeEndDate: this.timeframeEndDate,
state: this.state,
newEpic: true,
epics,
}),
);
sortEpics(this.state.epics, this.sortedBy);
}
getEpics() {
return this.state.epics;
}
getCurrentGroupId() {
return this.state.currentGroupId;
}
getTimeframe() {
return this.state.timeframe;
}
extendTimeframe(extendAs = EXTEND_AS.PREPEND) {
const timeframeToExtend = extendTimeframeForPreset({
presetType: this.presetType,
extendAs,
initialDate: extendAs === EXTEND_AS.PREPEND ? this.timeframeStartDate : this.timeframeEndDate,
});
if (extendAs === EXTEND_AS.PREPEND) {
this.state.timeframe.unshift(...timeframeToExtend);
} else {
this.state.timeframe.push(...timeframeToExtend);
}
this.initTimeframeThreshold();
this.state.epics.forEach(epic =>
RoadmapStore.processEpicDates(epic, this.timeframeStartDate, this.timeframeEndDate),
);
return timeframeToExtend;
}
static filterInvalidEpics({
epics,
timeframeStartDate,
timeframeEndDate,
state,
newEpic = false,
}) {
return epics.reduce((filteredEpics, epic) => {
const formattedEpic = RoadmapStore.formatEpicDetails(
epic,
timeframeStartDate,
timeframeEndDate,
);
// Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline
if (
formattedEpic.startDate <= formattedEpic.endDate &&
state.epicIds.indexOf(formattedEpic.id) < 0
) {
Object.assign(formattedEpic, {
newEpic,
});
filteredEpics.push(formattedEpic);
state.epicIds.push(formattedEpic.id);
}
return filteredEpics;
}, []);
}
/**
* This method constructs Epic object and assigns proxy dates
* in case start or end dates are unavailable.
*
* @param {Object} rawEpic
* @param {Date} timeframeStartDate
* @param {Date} timeframeEndDate
*/
static formatEpicDetails(rawEpic, timeframeStartDate, timeframeEndDate) {
const epicItem = convertObjectPropsToCamelCase(rawEpic);
if (rawEpic.start_date) {
// If startDate is present
const startDate = parsePikadayDate(rawEpic.start_date);
epicItem.startDate = startDate;
epicItem.originalStartDate = startDate;
} else {
// startDate is not available
epicItem.startDateUndefined = true;
}
if (rawEpic.end_date) {
// If endDate is present
const endDate = parsePikadayDate(rawEpic.end_date);
epicItem.endDate = endDate;
epicItem.originalEndDate = endDate;
} else {
// endDate is not available
epicItem.endDateUndefined = true;
}
RoadmapStore.processEpicDates(epicItem, timeframeStartDate, timeframeEndDate);
return epicItem;
}
static processEpicDates(epic, timeframeStartDate, timeframeEndDate) {
if (!epic.startDateUndefined) {
// If startDate is less than first timeframe item
if (epic.originalStartDate.getTime() < timeframeStartDate.getTime()) {
Object.assign(epic, {
// startDate is out of range
startDateOutOfRange: true,
// Use startDate object to set a proxy date so
// that timeline bar can render it.
startDate: newDate(timeframeStartDate),
});
} else {
Object.assign(epic, {
// startDate is within range
startDateOutOfRange: false,
// Set startDate to original startDate
startDate: newDate(epic.originalStartDate),
});
}
} else {
Object.assign(epic, {
startDate: newDate(timeframeStartDate),
});
}
if (!epic.endDateUndefined) {
// If endDate is greater than last timeframe item
if (epic.originalEndDate.getTime() > timeframeEndDate.getTime()) {
Object.assign(epic, {
// endDate is out of range
endDateOutOfRange: true,
// Use endDate object to set a proxy date so
// that timeline bar can render it.
endDate: newDate(timeframeEndDate),
});
} else {
Object.assign(epic, {
// startDate is within range
endDateOutOfRange: false,
// Set startDate to original startDate
endDate: newDate(epic.originalEndDate),
});
}
} else {
Object.assign(epic, {
endDate: newDate(timeframeEndDate),
});
}
}
}
export default () => ({ export default () => ({
// API Calls
basePath: '',
epicsState: '',
filterQueryString: '',
initialEpicsPath: '',
// Data
epics: [], epics: [],
epicIds: [], epicIds: [],
currentGroupId: -1, currentGroupId: -1,
timeframe: [], timeframe: [],
extendedTimeframe: [],
presetType: '', presetType: '',
sortedBy: '', sortedBy: '',
// UI Flags
epicsFetchInProgress: false,
epicsFetchForTimeframeInProgress: false,
epicsFetchFailure: false,
epicsFetchResultEmpty: false,
}); });
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
/**
* Updates provided `epic` object with necessary props
* representing underlying dates.
*
* @param {Object} epic
* @param {Date} timeframeStartDate
* @param {Date} timeframeEndDate
*/
export const processEpicDates = (epic, timeframeStartDate, timeframeEndDate) => {
if (!epic.startDateUndefined) {
// If startDate is less than first timeframe item
if (epic.originalStartDate.getTime() < timeframeStartDate.getTime()) {
Object.assign(epic, {
// startDate is out of range
startDateOutOfRange: true,
// Use startDate object to set a proxy date so
// that timeline bar can render it.
startDate: newDate(timeframeStartDate),
});
} else {
Object.assign(epic, {
// startDate is within range
startDateOutOfRange: false,
// Set startDate to original startDate
startDate: newDate(epic.originalStartDate),
});
}
} else {
Object.assign(epic, {
startDate: newDate(timeframeStartDate),
});
}
if (!epic.endDateUndefined) {
// If endDate is greater than last timeframe item
if (epic.originalEndDate.getTime() > timeframeEndDate.getTime()) {
Object.assign(epic, {
// endDate is out of range
endDateOutOfRange: true,
// Use endDate object to set a proxy date so
// that timeline bar can render it.
endDate: newDate(timeframeEndDate),
});
} else {
Object.assign(epic, {
// startDate is within range
endDateOutOfRange: false,
// Set startDate to original startDate
endDate: newDate(epic.originalEndDate),
});
}
} else {
Object.assign(epic, {
endDate: newDate(timeframeEndDate),
});
}
return epic;
};
/**
* Constructs Epic object with camelCase props and assigns proxy dates in case
* start or end dates are unavailable.
*
* @param {Object} rawEpic
* @param {Date} timeframeStartDate
* @param {Date} timeframeEndDate
*/
export const formatEpicDetails = (rawEpic, timeframeStartDate, timeframeEndDate) => {
const epicItem = convertObjectPropsToCamelCase(rawEpic);
if (rawEpic.start_date) {
// If startDate is present
const startDate = parsePikadayDate(rawEpic.start_date);
epicItem.startDate = startDate;
epicItem.originalStartDate = startDate;
} else {
// startDate is not available
epicItem.startDateUndefined = true;
}
if (rawEpic.end_date) {
// If endDate is present
const endDate = parsePikadayDate(rawEpic.end_date);
epicItem.endDate = endDate;
epicItem.originalEndDate = endDate;
} else {
// endDate is not available
epicItem.endDateUndefined = true;
}
processEpicDates(epicItem, timeframeStartDate, timeframeEndDate);
return epicItem;
};
import Vue from 'vue'; import Vue from 'vue';
import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue'; import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue';
import RoadmapStore from 'ee/roadmap/store/roadmap_store'; import createStore from 'ee/roadmap/store';
import eventHub from 'ee/roadmap/event_hub'; import eventHub from 'ee/roadmap/event_hub';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants'; import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { import {
rawEpics, mockShellWidth,
mockTimeframeInitialDate, mockTimeframeInitialDate,
mockGroupId, mockGroupId,
mockShellWidth, rawEpics,
mockSortedBy, mockSortedBy,
basePath,
epicsPath,
} from '../mock_data'; } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = new RoadmapStore({ const store = createStore();
groupId: mockGroupId, store.dispatch('setInitialData', {
presetType: PRESET_TYPES.MONTHS, currentGroupId: mockGroupId,
sortedBy: mockSortedBy, sortedBy: mockSortedBy,
presetType: PRESET_TYPES.MONTHS,
timeframe: mockTimeframeMonths, timeframe: mockTimeframeMonths,
filterQueryString: '',
initialEpicsPath: epicsPath,
basePath,
}); });
store.setEpics(rawEpics); store.dispatch('receiveEpicsSuccess', { rawEpics });
const mockEpics = store.getEpics();
const mockEpics = store.state.epics;
const createComponent = ({ const createComponent = ({
epics = mockEpics, epics = mockEpics,
......
...@@ -54,6 +54,8 @@ export const mockTimeframeQuartersAppend = [ ...@@ -54,6 +54,8 @@ export const mockTimeframeQuartersAppend = [
]; ];
export const mockTimeframeMonthsPrepend = [ export const mockTimeframeMonthsPrepend = [
new Date(2017, 2, 1),
new Date(2017, 3, 1),
new Date(2017, 4, 1), new Date(2017, 4, 1),
new Date(2017, 5, 1), new Date(2017, 5, 1),
new Date(2017, 6, 1), new Date(2017, 6, 1),
...@@ -66,7 +68,8 @@ export const mockTimeframeMonthsAppend = [ ...@@ -66,7 +68,8 @@ export const mockTimeframeMonthsAppend = [
new Date(2018, 7, 1), new Date(2018, 7, 1),
new Date(2018, 8, 1), new Date(2018, 8, 1),
new Date(2018, 9, 1), new Date(2018, 9, 1),
new Date(2018, 10, 30), new Date(2018, 10, 1),
new Date(2018, 11, 31),
]; ];
export const mockTimeframeWeeksPrepend = [ export const mockTimeframeWeeksPrepend = [
...@@ -101,6 +104,37 @@ export const mockEpic = { ...@@ -101,6 +104,37 @@ export const mockEpic = {
webUrl: '/groups/gitlab-org/-/epics/1', webUrl: '/groups/gitlab-org/-/epics/1',
}; };
export const mockRawEpic = {
id: 41,
iid: 2,
description: null,
title: 'Another marketing',
group_id: 56,
group_name: 'Marketing',
group_full_name: 'Gitlab Org / Marketing',
start_date: '2017-6-26',
end_date: '2018-03-10',
web_url: '/groups/gitlab-org/marketing/-/epics/2',
};
export const mockFormattedEpic = {
id: 41,
iid: 2,
description: null,
title: 'Another marketing',
groupId: 56,
groupName: 'Marketing',
groupFullName: 'Gitlab Org / Marketing',
startDate: new Date(2017, 10, 1),
originalStartDate: new Date(2017, 5, 26),
endDate: new Date(2018, 2, 10),
originalEndDate: new Date(2018, 2, 10),
startDateOutOfRange: true,
endDateOutOfRange: false,
webUrl: '/groups/gitlab-org/marketing/-/epics/2',
newEpic: undefined,
};
export const rawEpics = [ export const rawEpics = [
{ {
id: 41, id: 41,
......
import axios from '~/lib/utils/axios_utils';
import RoadmapService from 'ee/roadmap/service/roadmap_service';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import { basePath, epicsPath, mockTimeframeInitialDate } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
describe('RoadmapService', () => {
let service;
beforeEach(() => {
service = new RoadmapService({
initialEpicsPath: epicsPath,
epicsState: 'all',
filterQueryString: '',
basePath,
});
});
describe('getEpics', () => {
it('returns axios instance for Epics path', () => {
spyOn(axios, 'get').and.stub();
service.getEpics();
expect(axios.get).toHaveBeenCalledWith(
'/groups/gitlab-org/-/epics.json?start_date=2017-11-1&end_date=2018-4-30',
);
});
});
describe('getEpicsForTimeframe', () => {
it('calls `getEpicsPathForPreset` to construct epics path', () => {
const getEpicsPathSpy = spyOnDependency(RoadmapService, 'getEpicsPathForPreset');
spyOn(axios, 'get').and.stub();
const presetType = PRESET_TYPES.MONTHS;
service.getEpicsForTimeframe(presetType, mockTimeframeMonths);
expect(getEpicsPathSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
timeframe: mockTimeframeMonths,
epicsState: 'all',
filterQueryString: '',
basePath,
presetType,
}),
);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import * as actions from 'ee/roadmap/store/actions'; import * as actions from 'ee/roadmap/store/actions';
import * as types from 'ee/roadmap/store/mutation_types';
import defaultState from 'ee/roadmap/store/state';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { formatEpicDetails } from 'ee/roadmap/utils/epic_utils';
import { PRESET_TYPES, EXTEND_AS } from 'ee/roadmap/constants';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import {
mockGroupId,
basePath,
epicsPath,
mockTimeframeInitialDate,
mockTimeframeMonthsPrepend,
mockTimeframeMonthsAppend,
rawEpics,
mockRawEpic,
mockFormattedEpic,
mockSortedBy,
} from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
describe('Roadmap Vuex Actions', () => { describe('Roadmap Vuex Actions', () => {
const timeframeStartDate = mockTimeframeMonths[0];
const timeframeEndDate = mockTimeframeMonths[mockTimeframeMonths.length - 1];
let state;
beforeEach(() => {
state = Object.assign({}, defaultState(), {
groupId: mockGroupId,
timeframe: mockTimeframeMonths,
presetType: PRESET_TYPES.MONTHS,
sortedBy: mockSortedBy,
initialEpicsPath: epicsPath,
filterQueryString: '',
basePath,
timeframeStartDate,
timeframeEndDate,
});
});
describe('setInitialData', () => { describe('setInitialData', () => {
it('Should set initial roadmap props', done => { it('Should set initial roadmap props', done => {
const mockRoadmap = { const mockRoadmap = {
...@@ -14,7 +56,260 @@ describe('Roadmap Vuex Actions', () => { ...@@ -14,7 +56,260 @@ describe('Roadmap Vuex Actions', () => {
actions.setInitialData, actions.setInitialData,
mockRoadmap, mockRoadmap,
{}, {},
[{ type: 'SET_INITIAL_DATA', payload: mockRoadmap }], [{ type: types.SET_INITIAL_DATA, payload: mockRoadmap }],
[],
done,
);
});
});
describe('requestEpics', () => {
it('Should set `epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done);
});
});
describe('requestEpicsForTimeframe', () => {
it('Should set `epicsFetchForTimeframeInProgress` to true', done => {
testAction(
actions.requestEpicsForTimeframe,
{},
state,
[{ type: types.REQUEST_EPICS_FOR_TIMEFRAME }],
[],
done,
);
});
});
describe('receiveEpicsSuccess', () => {
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list', done => {
testAction(
actions.receiveEpicsSuccess,
{
rawEpics: [
Object.assign({}, mockRawEpic, {
start_date: '2017-12-31',
end_date: '2018-2-15',
}),
],
},
state,
[
{ type: types.UPDATE_EPIC_IDS, payload: mockRawEpic.id },
{
type: types.RECEIVE_EPICS_SUCCESS,
payload: [
Object.assign({}, mockFormattedEpic, {
startDateOutOfRange: false,
endDateOutOfRange: false,
startDate: new Date(2017, 11, 31),
originalStartDate: new Date(2017, 11, 31),
endDate: new Date(2018, 1, 15),
originalEndDate: new Date(2018, 1, 15),
}),
],
},
],
[],
done,
);
});
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list when timeframe was extended', done => {
testAction(
actions.receiveEpicsSuccess,
{ rawEpics: [mockRawEpic], newEpic: true, timeframeExtended: true },
state,
[
{ type: types.UPDATE_EPIC_IDS, payload: mockRawEpic.id },
{
type: types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS,
payload: [Object.assign({}, mockFormattedEpic, { newEpic: true })],
},
],
[],
done,
);
});
});
describe('receiveEpicsFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('Should set epicsFetchInProgress, epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', done => {
testAction(
actions.receiveEpicsFailure,
{},
state,
[{ type: types.RECEIVE_EPICS_FAILURE }],
[],
done,
);
});
it('Should show flash error', () => {
actions.receiveEpicsFailure({ commit: () => {} });
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'Something went wrong while fetching epics',
);
});
});
describe('fetchEpics', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('Should dispatch requestEpics and receiveEpicsSuccess when request is successful', done => {
mock.onGet(epicsPath).replyOnce(200, rawEpics);
testAction(
actions.fetchEpics,
null,
state,
[],
[
{
type: 'requestEpics',
},
{
type: 'receiveEpicsSuccess',
payload: { rawEpics },
},
],
done,
);
});
});
describe('failure', () => {
it('Should dispatch requestEpics and receiveEpicsFailure when request fails', done => {
mock.onGet(epicsPath).replyOnce(500, {});
testAction(
actions.fetchEpics,
null,
state,
[],
[
{
type: 'requestEpics',
},
{
type: 'receiveEpicsFailure',
},
],
done,
);
});
});
});
describe('fetchEpicsForTimeframe', () => {
const mockEpicsPath =
'/groups/gitlab-org/-/epics.json?state=&start_date=2017-11-1&end_date=2018-6-30';
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('Should dispatch requestEpicsForTimeframe and receiveEpicsSuccess when request is successful', done => {
mock.onGet(mockEpicsPath).replyOnce(200, rawEpics);
testAction(
actions.fetchEpicsForTimeframe,
{ timeframe: mockTimeframeMonths },
state,
[],
[
{
type: 'requestEpicsForTimeframe',
},
{
type: 'receiveEpicsSuccess',
payload: { rawEpics, newEpic: true, timeframeExtended: true },
},
],
done,
);
});
});
describe('failure', () => {
it('Should dispatch requestEpicsForTimeframe and requestEpicsFailure when request fails', done => {
mock.onGet(mockEpicsPath).replyOnce(500, {});
testAction(
actions.fetchEpicsForTimeframe,
{ timeframe: mockTimeframeMonths },
state,
[],
[
{
type: 'requestEpicsForTimeframe',
},
{
type: 'receiveEpicsFailure',
},
],
done,
);
});
});
});
describe('extendTimeframe', () => {
it('Should prepend to timeframe when called with extend type prepend', done => {
testAction(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.PREPEND },
state,
[{ type: types.PREPEND_TIMEFRAME, payload: mockTimeframeMonthsPrepend }],
[],
done,
);
});
it('Should append to timeframe when called with extend type append', done => {
testAction(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.APPEND },
state,
[{ type: types.APPEND_TIMEFRAME, payload: mockTimeframeMonthsAppend }],
[],
done,
);
});
});
describe('refreshEpicDates', () => {
it('Should update epics after refreshing epic dates to match with updated timeframe', done => {
const epics = rawEpics.map(epic =>
formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
);
testAction(
actions.refreshEpicDates,
{},
{ ...state, timeframe: mockTimeframeMonths.concat(mockTimeframeMonthsAppend), epics },
[{ type: types.SET_EPICS, payload: epics }],
[], [],
done, done,
); );
......
import mutations from 'ee/roadmap/store/mutations'; import mutations from 'ee/roadmap/store/mutations';
import * as types from 'ee/roadmap/store/mutation_types'; import * as types from 'ee/roadmap/store/mutation_types';
import defaultState from 'ee/roadmap/store/state';
import { mockGroupId, basePath, epicsPath, mockSortedBy } from '../mock_data';
describe('Roadmap Store Mutations', () => { describe('Roadmap Store Mutations', () => {
let state;
beforeEach(() => {
state = defaultState();
});
describe('SET_INITIAL_DATA', () => { describe('SET_INITIAL_DATA', () => {
it('Should set initial Roadmap data to state', () => { it('Should set initial Roadmap data to state', () => {
const state = {}; const initialData = {
const mockData = { epicsFetchInProgress: false,
foo: 'bar', epicsFetchForTimeframeInProgress: false,
bar: 'baz', epicsFetchFailure: false,
epicsFetchResultEmpty: false,
currentGroupId: mockGroupId,
sortedBy: mockSortedBy,
initialEpicsPath: epicsPath,
extendedTimeframe: [],
filterQueryString: '',
epicsState: 'all',
epicIds: [],
epics: [],
basePath,
}; };
mutations[types.SET_INITIAL_DATA](state, mockData); mutations[types.SET_INITIAL_DATA](state, initialData);
expect(state).toEqual(jasmine.objectContaining(initialData));
});
});
describe('SET_EPICS', () => {
it('Should provided epics array in state', () => {
const epics = [{ id: 1 }, { id: 2 }];
mutations[types.SET_EPICS](state, epics);
expect(state.epics).toEqual(epics);
});
});
describe('UPDATE_EPIC_IDS', () => {
it('Should insert provided epicId to epicIds array in state', () => {
mutations[types.UPDATE_EPIC_IDS](state, 22);
expect(state.epicIds.length).toBe(1);
expect(state.epicIds[0]).toBe(22);
});
});
describe('REQUEST_EPICS', () => {
it('Should set state.epicsFetchInProgress to `true`', () => {
mutations[types.REQUEST_EPICS](state);
expect(state.epicsFetchInProgress).toBe(true);
});
});
describe('REQUEST_EPICS_FOR_TIMEFRAME', () => {
it('Should set state.epicsFetchForTimeframeInProgress to `true`', () => {
mutations[types.REQUEST_EPICS_FOR_TIMEFRAME](state);
expect(state.epicsFetchForTimeframeInProgress).toBe(true);
});
});
describe('RECEIVE_EPICS_SUCCESS', () => {
it('Should set epicsFetchResultEmpty, epics in state based on provided epics array and set epicsFetchInProgress to `false`', () => {
const epics = [{ id: 1 }, { id: 2 }];
mutations[types.RECEIVE_EPICS_SUCCESS](state, epics);
expect(state.epicsFetchResultEmpty).toBe(false);
expect(state.epics).toEqual(epics);
expect(state.epicsFetchInProgress).toBe(false);
});
});
describe('RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS', () => {
it('Should set epics in state based on provided epics array and set epicsFetchForTimeframeInProgress to `false`', () => {
const epics = [{ id: 1 }, { id: 2 }];
mutations[types.RECEIVE_EPICS_FOR_TIMEFRAME_SUCCESS](state, epics);
expect(state.epics).toEqual(epics);
expect(state.epicsFetchForTimeframeInProgress).toBe(false);
});
});
describe('RECEIVE_EPICS_FAILURE', () => {
it('Should set epicsFetchInProgress & epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', () => {
mutations[types.RECEIVE_EPICS_FAILURE](state);
expect(state.epicsFetchInProgress).toBe(false);
expect(state.epicsFetchForTimeframeInProgress).toBe(false);
expect(state.epicsFetchFailure).toBe(true);
});
});
describe('PREPEND_TIMEFRAME', () => {
it('Should set extendedTimeframe to provided extendedTimeframe param and prepend it to timeframe array in state', () => {
state.timeframe.push('foo');
const extendedTimeframe = ['bar'];
mutations[types.PREPEND_TIMEFRAME](state, extendedTimeframe);
expect(state.extendedTimeframe).toBe(extendedTimeframe);
expect(state.timeframe[0]).toBe(extendedTimeframe[0]);
});
});
describe('APPEND_TIMEFRAME', () => {
it('Should set extendedTimeframe to provided extendedTimeframe param and append it to timeframe array in state', () => {
state.timeframe.push('foo');
const extendedTimeframe = ['bar'];
mutations[types.APPEND_TIMEFRAME](state, extendedTimeframe);
expect(state).toEqual(mockData); expect(state.extendedTimeframe).toBe(extendedTimeframe);
expect(state.timeframe[1]).toBe(extendedTimeframe[0]);
}); });
}); });
}); });
import RoadmapStore from 'ee/roadmap/store/roadmap_store';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES, EXTEND_AS } from 'ee/roadmap/constants';
import { mockGroupId, mockTimeframeInitialDate, rawEpics, mockSortedBy } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
describe('RoadmapStore', () => {
let store;
beforeEach(() => {
store = new RoadmapStore({
groupId: mockGroupId,
timeframe: mockTimeframeMonths,
presetType: PRESET_TYPES.MONTHS,
sortedBy: mockSortedBy,
});
});
describe('constructor', () => {
it('initializes default state', () => {
expect(store.state).toBeDefined();
expect(Array.isArray(store.state.epics)).toBe(true);
expect(Array.isArray(store.state.epicIds)).toBe(true);
expect(store.state.currentGroupId).toBe(mockGroupId);
expect(store.state.timeframe).toBe(mockTimeframeMonths);
expect(store.presetType).toBe(PRESET_TYPES.MONTHS);
expect(store.sortedBy).toBe(mockSortedBy);
expect(store.timeframeStartDate).toBeDefined();
expect(store.timeframeEndDate).toBeDefined();
});
});
describe('setEpics', () => {
it('sets Epics list to state while filtering out Epics with invalid dates', () => {
spyOn(RoadmapStore, 'filterInvalidEpics').and.callThrough();
store.setEpics(rawEpics);
expect(RoadmapStore.filterInvalidEpics).toHaveBeenCalledWith(
jasmine.objectContaining({
timeframeStartDate: store.timeframeStartDate,
timeframeEndDate: store.timeframeEndDate,
state: store.state,
epics: rawEpics,
}),
);
expect(store.getEpics().length).toBe(rawEpics.length - 1); // There is only 1 invalid epic
});
});
describe('addEpics', () => {
beforeEach(() => {
store.setEpics(rawEpics);
});
it('adds new Epics to the epics list within state while filtering out existing epics or epics with invalid dates and sorts the list based on `sortedBy` value', () => {
spyOn(RoadmapStore, 'filterInvalidEpics');
const sortEpicsSpy = spyOnDependency(RoadmapStore, 'sortEpics').and.stub();
const newEpic = Object.assign({}, rawEpics[0], {
id: 999,
});
store.addEpics([newEpic]);
expect(RoadmapStore.filterInvalidEpics).toHaveBeenCalledWith(
jasmine.objectContaining({
timeframeStartDate: store.timeframeStartDate,
timeframeEndDate: store.timeframeEndDate,
state: store.state,
epics: [newEpic],
newEpic: true,
}),
);
expect(sortEpicsSpy).toHaveBeenCalledWith(jasmine.any(Object), mockSortedBy);
// rawEpics contain 2 invalid epics but now that we added 1
// new epic, length will be `rawEpics.length - 1 (invalid) + 1 (new epic)`
expect(store.getEpics().length).toBe(rawEpics.length);
});
});
describe('getCurrentGroupId', () => {
it('gets currentGroupId from store state', () => {
expect(store.getCurrentGroupId()).toBe(mockGroupId);
});
});
describe('getTimeframe', () => {
it('gets timeframe from store state', () => {
expect(store.getTimeframe()).toBe(mockTimeframeMonths);
});
});
describe('extendTimeframe', () => {
beforeEach(() => {
store.setEpics(rawEpics);
store.state.timeframe = [];
});
it('calls `extendTimeframeForPreset` and prepends items to the timeframe when called with `extendAs` param as `prepend`', () => {
const extendTimeframeSpy = spyOnDependency(
RoadmapStore,
'extendTimeframeForPreset',
).and.returnValue([]);
spyOn(store.state.timeframe, 'unshift');
spyOn(store, 'initTimeframeThreshold');
spyOn(RoadmapStore, 'processEpicDates');
const itemCount = store.extendTimeframe(EXTEND_AS.PREPEND).length;
expect(extendTimeframeSpy).toHaveBeenCalledWith(jasmine.any(Object));
expect(store.state.timeframe.unshift).toHaveBeenCalled();
expect(store.initTimeframeThreshold).toHaveBeenCalled();
expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
expect(itemCount).toBe(0);
});
it('calls `extendTimeframeForPreset` and appends items to the timeframe when called with `extendAs` param as `append`', () => {
const extendTimeframeSpy = spyOnDependency(
RoadmapStore,
'extendTimeframeForPreset',
).and.returnValue([]);
spyOn(store.state.timeframe, 'push');
spyOn(store, 'initTimeframeThreshold');
spyOn(RoadmapStore, 'processEpicDates');
const itemCount = store.extendTimeframe(EXTEND_AS.APPEND).length;
expect(extendTimeframeSpy).toHaveBeenCalledWith(jasmine.any(Object));
expect(store.state.timeframe.push).toHaveBeenCalled();
expect(store.initTimeframeThreshold).toHaveBeenCalled();
expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
expect(itemCount).toBe(0);
});
});
describe('filterInvalidEpics', () => {
it('returns formatted epics list by filtering out epics with invalid dates', () => {
spyOn(RoadmapStore, 'formatEpicDetails').and.callThrough();
const epicsList = RoadmapStore.filterInvalidEpics({
epics: rawEpics,
timeframeStartDate: store.timeframeStartDate,
timeframeEndDate: store.timeframeEndDate,
state: store.state,
});
expect(RoadmapStore.formatEpicDetails).toHaveBeenCalled();
expect(epicsList.length).toBe(rawEpics.length - 1); // There are is only 1 invalid epic
});
it('returns formatted epics list by filtering out existing epics', () => {
store.setEpics(rawEpics);
spyOn(RoadmapStore, 'formatEpicDetails').and.callThrough();
const newEpic = Object.assign({}, rawEpics[0]);
const epicsList = RoadmapStore.filterInvalidEpics({
epics: [newEpic],
timeframeStartDate: store.timeframeStartDate,
timeframeEndDate: store.timeframeEndDate,
state: store.state,
});
expect(RoadmapStore.formatEpicDetails).toHaveBeenCalled();
expect(epicsList.length).toBe(0); // No epics are eligible to be added
});
});
describe('formatEpicDetails', () => {
const rawEpic = rawEpics[0];
it('returns formatted Epic object from raw Epic object', () => {
spyOn(RoadmapStore, 'processEpicDates');
const epic = RoadmapStore.formatEpicDetails(
rawEpic,
store.timeframeStartDate,
store.timeframeEndDate,
);
expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
expect(epic.id).toBe(rawEpic.id);
expect(epic.name).toBe(rawEpic.name);
expect(epic.groupId).toBe(rawEpic.group_id);
expect(epic.groupName).toBe(rawEpic.group_name);
});
it('returns formatted Epic object with startDateUndefined and proxy date set when start date is not available', () => {
const rawEpicWithoutSD = Object.assign({}, rawEpic, {
start_date: null,
});
const epic = RoadmapStore.formatEpicDetails(
rawEpicWithoutSD,
store.timeframeStartDate,
store.timeframeEndDate,
);
expect(epic.id).toBe(rawEpic.id);
expect(epic.startDateUndefined).toBe(true);
expect(epic.startDate.getTime()).toBe(store.timeframeStartDate.getTime());
});
it('returns formatted Epic object with endDateUndefined and proxy date set when end date is not available', () => {
const rawEpicWithoutED = Object.assign({}, rawEpic, {
end_date: null,
});
const epic = RoadmapStore.formatEpicDetails(
rawEpicWithoutED,
store.timeframeStartDate,
store.timeframeEndDate,
);
expect(epic.id).toBe(rawEpic.id);
expect(epic.endDateUndefined).toBe(true);
expect(epic.endDate.getTime()).toBe(store.timeframeEndDate.getTime());
});
it('returns formatted Epic object with startDateOutOfRange, proxy date and cached original start date set when start date is out of timeframe range', () => {
const rawStartDate = '2017-1-1';
const rawEpicSDOut = Object.assign({}, rawEpic, {
start_date: rawStartDate,
});
const epic = RoadmapStore.formatEpicDetails(
rawEpicSDOut,
store.timeframeStartDate,
store.timeframeEndDate,
);
expect(epic.id).toBe(rawEpic.id);
expect(epic.startDateOutOfRange).toBe(true);
expect(epic.startDate.getTime()).toBe(store.timeframeStartDate.getTime());
expect(epic.originalStartDate.getTime()).toBe(new Date(rawStartDate).getTime());
});
it('returns formatted Epic object with endDateOutOfRange, proxy date and cached original end date set when end date is out of timeframe range', () => {
const rawEndDate = '2019-1-1';
const rawEpicEDOut = Object.assign({}, rawEpic, {
end_date: rawEndDate,
});
const epic = RoadmapStore.formatEpicDetails(
rawEpicEDOut,
store.timeframeStartDate,
store.timeframeEndDate,
);
expect(epic.id).toBe(rawEpic.id);
expect(epic.endDateOutOfRange).toBe(true);
expect(epic.endDate.getTime()).toBe(store.timeframeEndDate.getTime());
expect(epic.originalEndDate.getTime()).toBe(new Date(rawEndDate).getTime());
});
});
});
import * as epicUtils from 'ee/roadmap/utils/epic_utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import { rawEpics } from '../mock_data';
describe('processEpicDates', () => {
const timeframeStartDate = new Date(2017, 0, 1);
const timeframeEndDate = new Date(2017, 11, 31);
it('Should set `startDateOutOfRange`/`endDateOutOfRange` as true and `startDate` and `endDate` to dates of timeframe range when epic dates are outside timeframe range', () => {
const mockEpic = {
originalStartDate: new Date(2016, 11, 15),
originalEndDate: new Date(2018, 0, 1),
};
const updatedEpic = epicUtils.processEpicDates(mockEpic, timeframeStartDate, timeframeEndDate);
expect(updatedEpic.startDateOutOfRange).toBe(true);
expect(updatedEpic.endDateOutOfRange).toBe(true);
expect(updatedEpic.startDate.getTime()).toBe(timeframeStartDate.getTime());
expect(updatedEpic.endDate.getTime()).toBe(timeframeEndDate.getTime());
});
it('Should set `startDateOutOfRange`/`endDateOutOfRange` as false and `startDate` and `endDate` to actual epic dates when they are within timeframe range', () => {
const mockEpic = {
originalStartDate: new Date(2017, 2, 10),
originalEndDate: new Date(2017, 6, 22),
};
const updatedEpic = epicUtils.processEpicDates(mockEpic, timeframeStartDate, timeframeEndDate);
expect(updatedEpic.startDateOutOfRange).toBe(false);
expect(updatedEpic.endDateOutOfRange).toBe(false);
expect(updatedEpic.startDate.getTime()).toBe(mockEpic.originalStartDate.getTime());
expect(updatedEpic.endDate.getTime()).toBe(mockEpic.originalEndDate.getTime());
});
it('Should set `startDate` and `endDate` to timeframe start and end dates when epic dates are undefined', () => {
const mockEpic = {
startDateUndefined: true,
endDateUndefined: true,
};
const updatedEpic = epicUtils.processEpicDates(mockEpic, timeframeStartDate, timeframeEndDate);
expect(updatedEpic.startDate.getTime()).toBe(timeframeStartDate.getTime());
expect(updatedEpic.endDate.getTime()).toBe(timeframeEndDate.getTime());
});
});
describe('formatEpicDetails', () => {
const timeframeStartDate = new Date(2017, 0, 1);
const timeframeEndDate = new Date(2017, 11, 31);
const rawEpic = rawEpics[0];
it('Should return formatted Epic object from raw Epic object', () => {
const epic = epicUtils.formatEpicDetails(rawEpic, timeframeStartDate, timeframeEndDate);
expect(epic.id).toBe(rawEpic.id);
expect(epic.name).toBe(rawEpic.name);
expect(epic.groupId).toBe(rawEpic.group_id);
expect(epic.groupName).toBe(rawEpic.group_name);
});
it('Should return formatted Epic object with `startDate`/`endDate` and `originalStartDate`/originalEndDate` initialized when dates are present', () => {
const mockRawEpic = {
start_date: '2017-2-15',
end_date: '2017-7-22',
};
const epic = epicUtils.formatEpicDetails(mockRawEpic, timeframeStartDate, timeframeEndDate);
const startDate = parsePikadayDate(mockRawEpic.start_date);
const endDate = parsePikadayDate(mockRawEpic.end_date);
expect(epic.startDate.getTime()).toBe(startDate.getTime());
expect(epic.originalStartDate.getTime()).toBe(startDate.getTime());
expect(epic.endDate.getTime()).toBe(endDate.getTime());
expect(epic.originalEndDate.getTime()).toBe(endDate.getTime());
});
it('Should return formatted Epic object with `startDateUndefined`/startDateUndefined` set to true when dates are null/undefined', () => {
const epic = epicUtils.formatEpicDetails({}, timeframeStartDate, timeframeEndDate);
expect(epic.originalStartDate).toBeUndefined();
expect(epic.originalEndDate).toBeUndefined();
expect(epic.startDateUndefined).toBe(true);
expect(epic.endDateUndefined).toBe(true);
});
});
...@@ -161,7 +161,7 @@ describe('getTimeframeForMonthsView', () => { ...@@ -161,7 +161,7 @@ describe('getTimeframeForMonthsView', () => {
describe('extendTimeframeForMonthsView', () => { describe('extendTimeframeForMonthsView', () => {
it('returns extended timeframe into the past from current timeframe startDate', () => { it('returns extended timeframe into the past from current timeframe startDate', () => {
const initialDate = mockTimeframeMonths[0]; const initialDate = mockTimeframeMonths[0];
const extendedTimeframe = extendTimeframeForMonthsView(initialDate, -6); const extendedTimeframe = extendTimeframeForMonthsView(initialDate, -8);
expect(extendedTimeframe.length).toBe(mockTimeframeMonthsPrepend.length); expect(extendedTimeframe.length).toBe(mockTimeframeMonthsPrepend.length);
extendedTimeframe.forEach((timeframeItem, index) => { extendedTimeframe.forEach((timeframeItem, index) => {
...@@ -171,7 +171,7 @@ describe('extendTimeframeForMonthsView', () => { ...@@ -171,7 +171,7 @@ describe('extendTimeframeForMonthsView', () => {
it('returns extended timeframe into the future from current timeframe endDate', () => { it('returns extended timeframe into the future from current timeframe endDate', () => {
const initialDate = mockTimeframeMonths[mockTimeframeMonths.length - 1]; const initialDate = mockTimeframeMonths[mockTimeframeMonths.length - 1];
const extendedTimeframe = extendTimeframeForMonthsView(initialDate, 7); const extendedTimeframe = extendTimeframeForMonthsView(initialDate, 8);
expect(extendedTimeframe.length).toBe(mockTimeframeMonthsAppend.length); expect(extendedTimeframe.length).toBe(mockTimeframeMonthsAppend.length);
extendedTimeframe.forEach((timeframeItem, index) => { extendedTimeframe.forEach((timeframeItem, index) => {
......
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