Commit d57f62c4 authored by Kushal Pandya's avatar Kushal Pandya

Use buffered rendering for Roadmap epics

Makes first load of Roadmap page faster by batch
rendering only those epics which are visible on
viewport. This commit also cleans up Roadmap by
using CSS instead of JS to set component sizes.
parent 08943e5e
...@@ -392,15 +392,21 @@ export const getTimeframeWindowFrom = (initialStartDate, length) => { ...@@ -392,15 +392,21 @@ export const getTimeframeWindowFrom = (initialStartDate, length) => {
* @param {Date} date * @param {Date} date
* @param {Array} quarter * @param {Array} quarter
*/ */
export const dayInQuarter = (date, quarter) => export const dayInQuarter = (date, quarter) => {
quarter.reduce((acc, month) => { const dateValues = {
if (date.getMonth() > month.getMonth()) { date: date.getDate(),
month: date.getMonth(),
};
return quarter.reduce((acc, month) => {
if (dateValues.month > month.getMonth()) {
return acc + totalDaysInMonth(month); return acc + totalDaysInMonth(month);
} else if (date.getMonth() === month.getMonth()) { } else if (dateValues.month === month.getMonth()) {
return acc + date.getDate(); return acc + dateValues.date;
} }
return acc + 0; return acc + 0;
}, 0); }, 0);
};
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
......
- page_classes = page_class << @html_class
- page_classes = page_classes.flatten.compact
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_classes }
= render "layouts/head" = render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data } %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
= render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_auto_complete" if @gfm_form
......
...@@ -28,14 +28,6 @@ export default { ...@@ -28,14 +28,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
}, },
updated() { updated() {
this.removeHighlight(); this.removeHighlight();
...@@ -75,8 +67,6 @@ export default { ...@@ -75,8 +67,6 @@ export default {
:timeframe="timeframe" :timeframe="timeframe"
:timeframe-item="timeframeItem" :timeframe-item="timeframeItem"
:epic="epic" :epic="epic"
:shell-width="shellWidth"
:item-width="itemWidth"
/> />
</div> </div>
</template> </template>
<script> <script>
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility'; import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
directives: {
tooltip,
},
props: { props: {
epic: { epic: {
type: Object, type: Object,
...@@ -78,22 +74,13 @@ export default { ...@@ -78,22 +74,13 @@ export default {
<template> <template>
<span class="epic-details-cell" data-qa-selector="epic_details_cell"> <span class="epic-details-cell" data-qa-selector="epic_details_cell">
<div class="epic-title"> <div class="epic-title">
<a v-tooltip :href="epic.webUrl" :title="epic.title" data-container="body" class="epic-url"> <a :href="epic.webUrl" :title="epic.title" class="epic-url">{{ epic.title }}</a>
{{ epic.title }}
</a>
</div> </div>
<div class="epic-group-timeframe"> <div class="epic-group-timeframe">
<span <span v-if="isEpicGroupDifferent" :title="epic.groupFullName" class="epic-group"
v-if="isEpicGroupDifferent" >{{ epic.groupName }} &middot;</span
v-tooltip
:title="epic.groupFullName"
class="epic-group"
data-placement="right"
data-container="body"
> >
{{ epic.groupName }} &middot; <span class="epic-timeframe" v-html="timeframeString"></span>
</span>
<span class="epic-timeframe" v-html="timeframeString"> </span>
</div> </div>
</span> </span>
</template> </template>
...@@ -5,11 +5,10 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin'; ...@@ -5,11 +5,10 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin'; import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin'; import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
import eventHub from '../event_hub'; import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
export default { export default {
cellWidth: TIMELINE_CELL_MIN_WIDTH,
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -31,56 +30,28 @@ export default { ...@@ -31,56 +30,28 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
}, },
data() { data() {
const { startDate, endDate } = this.epic;
return { return {
timelineBarReady: false, epicStartDateValues: {
timelineBarStyles: '', day: startDate.getDay(),
}; date: startDate.getDate(),
month: startDate.getMonth(),
year: startDate.getFullYear(),
time: startDate.getTime(),
},
epicEndDateValues: {
day: endDate.getDay(),
date: endDate.getDate(),
month: endDate.getMonth(),
year: endDate.getFullYear(),
time: endDate.getTime(),
}, },
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
}; };
}, },
showTimelineBar() { computed: {
return this.hasStartDate();
},
},
watch: {
shellWidth() {
// Render timeline bar only when shellWidth is updated.
this.renderTimelineBar();
},
},
mounted() {
eventHub.$on('refreshTimeline', this.renderTimelineBar);
},
beforeDestroy() {
eventHub.$off('refreshTimeline', this.renderTimelineBar);
},
methods: {
/**
* Gets cell width based on total number months for
* current timeframe and shellWidth excluding details cell width.
*
* In case cell width is too narrow, we have fixed minimum
* cell width (TIMELINE_CELL_MIN_WIDTH) to obey.
*/
getCellWidth() {
const minWidth = (this.shellWidth - EPIC_DETAILS_CELL_WIDTH) / this.timeframe.length;
return Math.max(minWidth, TIMELINE_CELL_MIN_WIDTH);
},
hasStartDate() { hasStartDate() {
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetType === PRESET_TYPES.QUARTERS) {
return this.hasStartDateForQuarter(); return this.hasStartDateForQuarter();
...@@ -91,35 +62,33 @@ export default { ...@@ -91,35 +62,33 @@ export default {
} }
return false; return false;
}, },
/** timelineBarStyles() {
* Renders timeline bar only if current let barStyles = {};
* timeframe item has startDate for the epic.
*/ if (this.hasStartDate) {
renderTimelineBar() {
if (this.hasStartDate()) {
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetType === PRESET_TYPES.QUARTERS) {
// CSS properties are a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/24 // CSS properties are a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/24
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`; barStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetType === PRESET_TYPES.MONTHS) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`; barStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`;
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetType === PRESET_TYPES.WEEKS) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`; barStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`;
} }
this.timelineBarReady = true;
} }
return barStyles;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span :style="itemStyles" class="epic-timeline-cell" data-qa-selector="epic_timeline_cell"> <span class="epic-timeline-cell" data-qa-selector="epic_timeline_cell">
<div class="timeline-bar-wrapper"> <div class="timeline-bar-wrapper">
<a <a
v-if="showTimelineBar" v-if="hasStartDate"
:href="epic.webUrl" :href="epic.webUrl"
:class="{ :class="{
'start-date-undefined': epic.startDateUndefined, 'start-date-undefined': epic.startDateUndefined,
...@@ -127,8 +96,7 @@ export default { ...@@ -127,8 +96,7 @@ export default {
}" }"
:style="timelineBarStyles" :style="timelineBarStyles"
class="timeline-bar" class="timeline-bar"
> ></a>
</a>
</div> </div>
</span> </span>
</template> </template>
<script> <script>
import eventHub from '../event_hub'; import { mapState, mapActions } from 'vuex';
import VirtualList from 'vue-virtual-scroll-list';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SectionMixin from '../mixins/section_mixin'; import eventHub from '../event_hub';
import { TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
import epicItem from './epic_item.vue'; import EpicItem from './epic_item.vue';
export default { export default {
EpicItem,
epicItemHeight: EPIC_ITEM_HEIGHT,
components: { components: {
epicItem, VirtualList,
EpicItem,
}, },
mixins: [SectionMixin], mixins: [glFeatureFlagsMixin()],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -29,33 +35,23 @@ export default { ...@@ -29,33 +35,23 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
shellHeight: 0,
emptyRowHeight: 0,
showEmptyRow: false,
offsetLeft: 0, offsetLeft: 0,
emptyRowContainerStyles: {},
showBottomShadow: false, showBottomShadow: false,
roadmapShellEl: null,
}; };
}, },
computed: { computed: {
emptyRowContainerStyles() { ...mapState(['bufferSize']),
return { emptyRowContainerVisible() {
height: `${this.emptyRowHeight}px`, return this.epics.length < this.bufferSize;
};
}, },
emptyRowCellStyles() { sectionContainerStyles() {
return { return {
width: `${this.sectionItemWidth}px`, width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
}; };
}, },
shadowCellStyles() { shadowCellStyles() {
...@@ -64,123 +60,97 @@ export default { ...@@ -64,123 +60,97 @@ export default {
}; };
}, },
}, },
watch: {
shellWidth: function shellWidth() {
// Scroll view to today indicator only when shellWidth is updated.
this.scrollToTodayIndicator();
// Initialize offsetLeft when shellWidth is updated
this.offsetLeft = this.$el.parentElement.offsetLeft;
},
},
mounted() { mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll); eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleTimelineRefresh);
this.$nextTick(() => {
this.initMounted(); this.initMounted();
});
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll); eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleTimelineRefresh);
}, },
methods: { methods: {
...mapActions(['setBufferSize']),
initMounted() { initMounted() {
// Get available shell height based on viewport height this.roadmapShellEl = this.$root.$el && this.$root.$el.firstChild;
this.shellHeight = window.innerHeight - this.$el.offsetTop; this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
// In case there are epics present, initialize empty row // Wait for component render to complete
if (this.epics.length) { this.$nextTick(() => {
this.initEmptyRow(); this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0;
}
eventHub.$emit('epicsListRendered', { // We cannot scroll to the indicator immediately
width: this.$el.clientWidth, // on render as it will trigger scroll event leading
height: this.shellHeight, // to timeline expand, so we wait for another render
// cycle to complete.
this.$nextTick(() => {
this.scrollToTodayIndicator();
}); });
},
/**
* In case number of epics in the list are not sufficient
* to fill in full page height, we need to show an empty row
* at the bottom with fixed absolute height such that the
* column rulers expand to full page height
*
* This method calculates absolute height for empty column in pixels
* based on height of available list items and sets it to component
* props.
*/
initEmptyRow() {
const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
// Check if approximate height is greater than shell height if (!Object.keys(this.emptyRowContainerStyles).length) {
if (approxChildrenHeight < this.shellHeight) { this.emptyRowContainerStyles = this.getEmptyRowContainerStyles();
// reset approximate height and recalculate actual height }
approxChildrenHeight = 0;
children.forEach(child => {
// accumulate children height
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
}); });
},
// set height and show empty row reducing horizontal scrollbar size getEmptyRowContainerStyles() {
this.emptyRowHeight = this.shellHeight - approxChildrenHeight; if (this.$refs.epicItems && this.$refs.epicItems.length) {
this.showEmptyRow = true; return {
} else { height: `${this.$el.clientHeight -
this.showBottomShadow = true; this.epics.length * this.$refs.epicItems[0].$el.clientHeight}px`,
};
} }
return {};
}, },
/** /**
* Scroll timeframe to the right of the timeline * Scroll timeframe to the right of the timeline
* by half the column size * by half the column size
*/ */
scrollToTodayIndicator() { scrollToTodayIndicator() {
this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0); if (this.$el.parentElement) this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
/**
* Method to update list section when refreshTimeline event
* is emitted on eventHub
*
* This method ensures that empty row is toggled
* based on whether list has enough epics to make
* list vertically scrollable.
*/
handleTimelineRefresh({ initialRender = false }) {
// Initialize emptyRow only once.
if (initialRender) {
this.initEmptyRow();
}
// Check if container height is less than total height of all Epic
// items combined (AKA list is scrollable).
const offsetHeight = this.$el.parentElement
? this.$el.parentElement.offsetHeight
: this.$el.offsetHeight;
const isListVertScrollable = offsetHeight < EPIC_ITEM_HEIGHT * (this.epics.length + 1);
// Toggle empty row.
this.showEmptyRow = !isListVertScrollable;
}, },
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) { handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight; this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
}, },
getEpicItemProps(index) {
return {
key: index,
props: {
epic: this.epics[index],
presetType: this.presetType,
timeframe: this.timeframe,
currentGroupId: this.currentGroupId,
},
};
},
}, },
}; };
</script> </script>
<template> <template>
<div :style="sectionContainerStyles" class="epics-list-section"> <div :style="sectionContainerStyles" class="epics-list-section">
<template v-if="glFeatures.roadmapBufferedRendering && !emptyRowContainerVisible">
<virtual-list
v-if="epics.length"
:size="$options.epicItemHeight"
:remain="bufferSize"
:bench="bufferSize"
:scrollelement="roadmapShellEl"
:item="$options.EpicItem"
:itemcount="epics.length"
:itemprops="getEpicItemProps"
/>
</template>
<template v-else>
<epic-item <epic-item
v-for="(epic, index) in epics" v-for="(epic, index) in epics"
ref="epicItems"
:key="index" :key="index"
:preset-type="presetType" :preset-type="presetType"
:epic="epic" :epic="epic"
:timeframe="timeframe" :timeframe="timeframe"
:current-group-id="currentGroupId" :current-group-id="currentGroupId"
:shell-width="sectionShellWidth"
:item-width="sectionItemWidth"
/> />
</template>
<div <div
v-if="showEmptyRow" v-if="emptyRowContainerVisible"
:style="emptyRowContainerStyles" :style="emptyRowContainerStyles"
class="epics-list-item epics-list-item-empty clearfix" class="epics-list-item epics-list-item-empty clearfix"
> >
...@@ -188,11 +158,9 @@ export default { ...@@ -188,11 +158,9 @@ export default {
<span <span
v-for="(timeframeItem, index) in timeframe" v-for="(timeframeItem, index) in timeframe"
:key="index" :key="index"
:style="emptyRowCellStyles"
class="epic-timeline-cell" class="epic-timeline-cell"
> ></span>
</span>
</div> </div>
<div v-if="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div> <div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
</div> </div>
</template> </template>
...@@ -20,10 +20,6 @@ export default { ...@@ -20,10 +20,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
itemWidth: {
type: Number,
required: true,
},
}, },
data() { data() {
const currentDate = new Date(); const currentDate = new Date();
...@@ -36,11 +32,6 @@ export default { ...@@ -36,11 +32,6 @@ export default {
}; };
}, },
computed: { computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
timelineHeaderLabel() { timelineHeaderLabel() {
const year = this.timeframeItem.getFullYear(); const year = this.timeframeItem.getFullYear();
const month = monthInWords(this.timeframeItem, true); const month = monthInWords(this.timeframeItem, true);
...@@ -85,7 +76,7 @@ export default { ...@@ -85,7 +76,7 @@ export default {
</script> </script>
<template> <template>
<span :style="itemStyles" class="timeline-header-item"> <span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div> <div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<months-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" /> <months-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span> </span>
......
...@@ -18,10 +18,6 @@ export default { ...@@ -18,10 +18,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
itemWidth: {
type: Number,
required: true,
},
}, },
data() { data() {
const currentDate = new Date(); const currentDate = new Date();
...@@ -32,11 +28,6 @@ export default { ...@@ -32,11 +28,6 @@ export default {
}; };
}, },
computed: { computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
quarterBeginDate() { quarterBeginDate() {
return this.timeframeItem.range[0]; return this.timeframeItem.range[0];
}, },
...@@ -66,7 +57,7 @@ export default { ...@@ -66,7 +57,7 @@ export default {
</script> </script>
<template> <template>
<span :style="itemStyles" class="timeline-header-item"> <span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div> <div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<quarters-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" /> <quarters-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span> </span>
......
...@@ -20,10 +20,6 @@ export default { ...@@ -20,10 +20,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
itemWidth: {
type: Number,
required: true,
},
}, },
data() { data() {
const currentDate = new Date(); const currentDate = new Date();
...@@ -34,11 +30,6 @@ export default { ...@@ -34,11 +30,6 @@ export default {
}; };
}, },
computed: { computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
lastDayOfCurrentWeek() { lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime()); const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7); lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
...@@ -77,7 +68,7 @@ export default { ...@@ -77,7 +68,7 @@ export default {
</script> </script>
<template> <template>
<span :style="itemStyles" class="timeline-header-item"> <span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div> <div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" /> <weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span> </span>
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import _ from 'underscore';
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';
...@@ -34,7 +33,6 @@ export default { ...@@ -34,7 +33,6 @@ export default {
data() { data() {
const roadmapGraphQL = gon.features && gon.features.roadmapGraphql; const roadmapGraphQL = gon.features && gon.features.roadmapGraphql;
return { return {
handleResizeThrottled: {},
// TODO // TODO
// Remove these method alias and call actual // Remove these method alias and call actual
// method once feature flag is removed. // method once feature flag is removed.
...@@ -73,25 +71,8 @@ export default { ...@@ -73,25 +71,8 @@ export default {
); );
}, },
}, },
watch: {
epicsFetchInProgress(value) {
if (!value && this.epics.length) {
this.$nextTick(() => {
eventHub.$emit('refreshTimeline', {
todayBarReady: true,
initialRender: true,
});
});
}
},
},
mounted() { mounted() {
this.fetchEpicsFn(); this.fetchEpicsFn();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -103,28 +84,6 @@ export default { ...@@ -103,28 +84,6 @@ export default {
'extendTimeframe', 'extendTimeframe',
'refreshEpicDates', 'refreshEpicDates',
]), ]),
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
*
* - RoadmapTimelineSection
* - TimelineTodayIndicator
* - EpicItemTimeline
*
* And hence when window is resized, any size attributes passed
* down to child components are no longer valid, so best approach
* to refresh entire app is to re-render it on resize, hence
* we toggle `windowResizeInProgress` variable which is bound
* to `RoadmapShell`.
*/
handleResize() {
this.setWindowResizeInProgress(true);
// We need to debounce the toggle to make sure loading animation
// shows up while app is being rerendered.
_.debounce(() => {
this.setWindowResizeInProgress(false);
}, 200)();
},
/** /**
* Once timeline is expanded (either with prepend or append) * Once timeline is expanded (either with prepend or append)
* We need performing following actions; * We need performing following actions;
......
...@@ -3,7 +3,7 @@ import { mapState } from 'vuex'; ...@@ -3,7 +3,7 @@ import { mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlSkeletonLoading } from '@gitlab/ui';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, EXTEND_AS } from '../constants'; import { EXTEND_AS } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import epicsListSection from './epics_list_section.vue'; import epicsListSection from './epics_list_section.vue';
...@@ -35,50 +35,14 @@ export default { ...@@ -35,50 +35,14 @@ export default {
}, },
data() { data() {
return { return {
shellWidth: 0,
shellHeight: 0,
noScroll: false,
timeframeStartOffset: 0, timeframeStartOffset: 0,
}; };
}, },
computed: { computed: {
...mapState(['defaultInnerHeight']), ...mapState(['defaultInnerHeight']),
containerStyles() {
return {
width: `${this.shellWidth}px`,
height: `${this.shellHeight}px`,
};
},
},
watch: {
/**
* Watcher to monitor whether epics list is long enough
* to allow vertical list scrolling.
*
* In case of scrollable list, we don't want vertical scrollbar
* to be visible, so we mask the scrollbar by increasing shell
* width past the scrollbar size.
*/
noScroll(value) {
if (this.$el.parentElement) {
this.shellWidth = this.getShellWidth(value);
}
},
}, },
mounted() { mounted() {
eventHub.$on('refreshTimeline', this.handleEpicsListRendered);
this.$nextTick(() => { this.$nextTick(() => {
// Client width at the time of component mount will not
// provide accurate size of viewport until child contents are
// actually loaded and rendered into the DOM, hence
// we wait for nextTick which ensures DOM update has completed
// before setting shellWidth
// see https://vuejs.org/v2/api/#Vue-nextTick
if (this.$el.parentElement) {
this.shellHeight = (this.defaultInnerHeight || window.innerHeight) - this.$el.offsetTop;
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
this.shellWidth = this.getShellWidth(this.noScroll);
// We're guarding this as in tests, `roadmapTimeline` // We're guarding this as in tests, `roadmapTimeline`
// is not ready when this line is executed. // is not ready when this line is executed.
if (this.$refs.roadmapTimeline) { if (this.$refs.roadmapTimeline) {
...@@ -87,19 +51,9 @@ export default { ...@@ -87,19 +51,9 @@ export default {
.querySelector('.item-sublabel .sublabel-value:first-child') .querySelector('.item-sublabel .sublabel-value:first-child')
.getBoundingClientRect().left; .getBoundingClientRect().left;
} }
}
}); });
}, },
beforeDestroy() {
eventHub.$off('refreshTimeline', this.handleEpicsListRendered);
},
methods: { methods: {
getShellWidth(noScroll) {
return this.$el.parentElement.clientWidth + (noScroll ? 0 : SCROLL_BAR_SIZE);
},
handleEpicsListRendered() {
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
},
handleScroll() { handleScroll() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el; const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
...@@ -124,20 +78,12 @@ export default { ...@@ -124,20 +78,12 @@ export default {
</script> </script>
<template> <template>
<div <div class="roadmap-shell" data-qa-selector="roadmap_shell" @scroll="handleScroll">
:class="{ 'prevent-vertical-scroll': noScroll }"
:style="containerStyles"
class="roadmap-shell"
data-qa-selector="roadmap_shell"
@scroll="handleScroll"
>
<roadmap-timeline-section <roadmap-timeline-section
ref="roadmapTimeline" ref="roadmapTimeline"
:preset-type="presetType" :preset-type="presetType"
:epics="epics" :epics="epics"
:timeframe="timeframe" :timeframe="timeframe"
:shell-width="shellWidth"
:list-scrollable="!noScroll"
/> />
<div v-if="!epics.length" class="skeleton-loader js-skeleton-loader"> <div v-if="!epics.length" class="skeleton-loader js-skeleton-loader">
<div v-for="n in 10" :key="n" class="mt-2"> <div v-for="n in 10" :key="n" class="mt-2">
...@@ -145,12 +91,11 @@ export default { ...@@ -145,12 +91,11 @@ export default {
</div> </div>
</div> </div>
<epics-list-section <epics-list-section
v-else
:preset-type="presetType" :preset-type="presetType"
:epics="epics" :epics="epics"
:timeframe="timeframe" :timeframe="timeframe"
:shell-width="shellWidth"
:current-group-id="currentGroupId" :current-group-id="currentGroupId"
:list-scrollable="!noScroll"
/> />
</div> </div>
</template> </template>
<script> <script>
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { PRESET_TYPES } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import SectionMixin from '../mixins/section_mixin';
import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue'; import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue';
import MonthsHeaderItem from './preset_months/months_header_item.vue'; import MonthsHeaderItem from './preset_months/months_header_item.vue';
...@@ -15,7 +13,6 @@ export default { ...@@ -15,7 +13,6 @@ export default {
MonthsHeaderItem, MonthsHeaderItem,
WeeksHeaderItem, WeeksHeaderItem,
}, },
mixins: [SectionMixin],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -29,14 +26,6 @@ export default { ...@@ -29,14 +26,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -54,6 +43,11 @@ export default { ...@@ -54,6 +43,11 @@ export default {
} }
return ''; return '';
}, },
sectionContainerStyles() {
return {
width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
};
},
}, },
mounted() { mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll); eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
...@@ -84,7 +78,6 @@ export default { ...@@ -84,7 +78,6 @@ export default {
:timeframe-index="index" :timeframe-index="index"
:timeframe-item="timeframeItem" :timeframe-item="timeframeItem"
:timeframe="timeframe" :timeframe="timeframe"
:item-width="sectionItemWidth"
/> />
</div> </div>
</template> </template>
<script> <script>
import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility'; import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK, SCROLL_BAR_SIZE } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -22,29 +22,22 @@ export default { ...@@ -22,29 +22,22 @@ export default {
}, },
data() { data() {
return { return {
todayBarStyles: '', todayBarStyles: {},
todayBarReady: false, todayBarReady: true,
}; };
}, },
mounted() { mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll); eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleEpicsListRender); this.$nextTick(() => {
this.todayBarStyles = this.getTodayBarStyles();
});
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll); eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleEpicsListRender);
}, },
methods: { methods: {
/** getTodayBarStyles() {
* This method takes height of current shell let left;
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ todayBarReady }) {
let left = 0;
let height = 0;
// Get total days of current timeframe Item and then // Get total days of current timeframe Item and then
// get size in % from current date and days in range // get size in % from current date and days in range
...@@ -63,21 +56,10 @@ export default { ...@@ -63,21 +56,10 @@ export default {
left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK); left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK);
} }
// On initial load, container element height is 0 return {
if (this.$root.$el.clientHeight === 0) {
height = window.innerHeight - this.$root.$el.offsetTop;
} else {
// When list is scrollable both vertically and horizontally
// We set height using top-level parent container height & position of
// today indicator element container.
height = this.$root.$el.clientHeight - this.$el.parentElement.offsetTop;
}
this.todayBarStyles = {
height: `${height}px`,
left: `${left}%`, left: `${left}%`,
height: `calc(100vh - ${this.$el.getBoundingClientRect().y + SCROLL_BAR_SIZE}px)`,
}; };
this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
}, },
handleEpicsListScroll() { handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x; const indicatorX = this.$el.getBoundingClientRect().x;
...@@ -91,5 +73,5 @@ export default { ...@@ -91,5 +73,5 @@ export default {
</script> </script>
<template> <template>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"> </span> <span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"></span>
</template> </template>
...@@ -6,12 +6,14 @@ export const EPIC_ITEM_HEIGHT = 50; ...@@ -6,12 +6,14 @@ export const EPIC_ITEM_HEIGHT = 50;
export const TIMELINE_CELL_MIN_WIDTH = 180; export const TIMELINE_CELL_MIN_WIDTH = 180;
export const SCROLL_BAR_SIZE = 15; export const SCROLL_BAR_SIZE = 16;
export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000; export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000;
export const DAYS_IN_WEEK = 7; export const DAYS_IN_WEEK = 7;
export const BUFFER_OVERLAP_SIZE = 20;
export const PRESET_TYPES = { export const PRESET_TYPES = {
QUARTERS: 'QUARTERS', QUARTERS: 'QUARTERS',
MONTHS: 'MONTHS', MONTHS: 'MONTHS',
......
...@@ -7,18 +7,18 @@ export default { ...@@ -7,18 +7,18 @@ export default {
*/ */
hasStartDateForMonth() { hasStartDateForMonth() {
return ( return (
this.epic.startDate.getMonth() === this.timeframeItem.getMonth() && this.epicStartDateValues.month === this.timeframeItem.getMonth() &&
this.epic.startDate.getFullYear() === this.timeframeItem.getFullYear() this.epicStartDateValues.year === this.timeframeItem.getFullYear()
); );
}, },
/** /**
* Check if current epic ends within current month (timeline cell) * Check if current epic ends within current month (timeline cell)
*/ */
isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate) { isTimeframeUnderEndDateForMonth(timeframeItem) {
if (epicEndDate.getFullYear() <= timeframeItem.getFullYear()) { if (this.epicEndDateValues.year <= timeframeItem.getFullYear()) {
return epicEndDate.getMonth() === timeframeItem.getMonth(); return this.epicEndDateValues.month === timeframeItem.getMonth();
} }
return epicEndDate.getTime() < timeframeItem.getTime(); return this.epicEndDateValues.time < timeframeItem.getTime();
}, },
/** /**
* Return timeline bar width for current month (timeline cell) based on * Return timeline bar width for current month (timeline cell) based on
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
*/ */
getTimelineBarStartOffsetForMonths() { getTimelineBarStartOffsetForMonths() {
const daysInMonth = totalDaysInMonth(this.timeframeItem); const daysInMonth = totalDaysInMonth(this.timeframeItem);
const startDate = this.epic.startDate.getDate(); const startDate = this.epicStartDateValues.date;
if ( if (
this.epic.startDateOutOfRange || this.epic.startDateOutOfRange ||
...@@ -88,9 +88,9 @@ export default { ...@@ -88,9 +88,9 @@ export default {
let timelineBarWidth = 0; let timelineBarWidth = 0;
const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem); const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth(); const { cellWidth } = this.$options;
const epicStartDate = this.epic.startDate; const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epic.endDate; const epicEndDate = this.epicEndDateValues;
// Start iteration from current month // Start iteration from current month
for (let i = indexOfCurrentMonth; i < this.timeframe.length; i += 1) { for (let i = indexOfCurrentMonth; i < this.timeframe.length; i += 1) {
...@@ -99,13 +99,13 @@ export default { ...@@ -99,13 +99,13 @@ export default {
if (i === indexOfCurrentMonth) { if (i === indexOfCurrentMonth) {
// If this is current month // If this is current month
if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i], epicEndDate)) { if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i])) {
// If Epic endDate falls under the range of current timeframe month // If Epic endDate falls under the range of current timeframe month
// then get width for number of days between start and end dates (inclusive) // then get width for number of days between start and end dates (inclusive)
timelineBarWidth += this.getBarWidthForSingleMonth( timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth, cellWidth,
daysInMonth, daysInMonth,
epicEndDate.getDate() - epicStartDate.getDate() + 1, epicEndDate.date - epicStartDate.date + 1,
); );
// Break as Epic start and end date fall within current timeframe month itself! // Break as Epic start and end date fall within current timeframe month itself!
break; break;
...@@ -115,18 +115,17 @@ export default { ...@@ -115,18 +115,17 @@ export default {
// If start date is first day of the month, // If start date is first day of the month,
// we need width of full cell (i.e. total days of month) // we need width of full cell (i.e. total days of month)
// otherwise, we need width only for date from total days of month. // otherwise, we need width only for date from total days of month.
const date = const date = epicStartDate.date === 1 ? daysInMonth : daysInMonth - epicStartDate.date;
epicStartDate.getDate() === 1 ? daysInMonth : daysInMonth - epicStartDate.getDate();
timelineBarWidth += this.getBarWidthForSingleMonth(cellWidth, daysInMonth, date); timelineBarWidth += this.getBarWidthForSingleMonth(cellWidth, daysInMonth, date);
} }
} else if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i], epicEndDate)) { } else if (this.isTimeframeUnderEndDateForMonth(this.timeframe[i])) {
// If this is NOT current month but epicEndDate falls under // If this is NOT current month but epicEndDate falls under
// current timeframe month then calculate width // current timeframe month then calculate width
// based on date of the month // based on date of the month
timelineBarWidth += this.getBarWidthForSingleMonth( timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth, cellWidth,
daysInMonth, daysInMonth,
epicEndDate.getDate(), epicEndDate.date,
); );
// Break as Epic end date falls within current timeframe month! // Break as Epic end date falls within current timeframe month!
break; break;
......
...@@ -10,17 +10,17 @@ export default { ...@@ -10,17 +10,17 @@ export default {
const quarterEnd = this.timeframeItem.range[2]; const quarterEnd = this.timeframeItem.range[2];
return ( return (
this.epic.startDate.getTime() >= quarterStart.getTime() && this.epicStartDateValues.time >= quarterStart.getTime() &&
this.epic.startDate.getTime() <= quarterEnd.getTime() this.epicStartDateValues.time <= quarterEnd.getTime()
); );
}, },
/** /**
* Check if current epic ends within current quarter (timeline cell) * Check if current epic ends within current quarter (timeline cell)
*/ */
isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate) { isTimeframeUnderEndDateForQuarter(timeframeItem) {
const quarterEnd = timeframeItem.range[2]; const quarterEnd = timeframeItem.range[2];
return epicEndDate.getTime() <= quarterEnd.getTime(); return this.epicEndDateValues.time <= quarterEnd.getTime();
}, },
/** /**
* Return timeline bar width for current quarter (timeline cell) based on * Return timeline bar width for current quarter (timeline cell) based on
...@@ -89,7 +89,7 @@ export default { ...@@ -89,7 +89,7 @@ export default {
let timelineBarWidth = 0; let timelineBarWidth = 0;
const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem); const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth(); const { cellWidth } = this.$options;
const epicStartDate = this.epic.startDate; const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate; const epicEndDate = this.epic.endDate;
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
const currentQuarter = this.timeframe[i].range; const currentQuarter = this.timeframe[i].range;
if (i === indexOfCurrentQuarter) { if (i === indexOfCurrentQuarter) {
if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) { if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter( timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth, cellWidth,
totalDaysInQuarter(currentQuarter), totalDaysInQuarter(currentQuarter),
...@@ -117,7 +117,7 @@ export default { ...@@ -117,7 +117,7 @@ export default {
date, date,
); );
} }
} else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) { } else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter( timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth, cellWidth,
totalDaysInQuarter(currentQuarter), totalDaysInQuarter(currentQuarter),
......
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, SCROLL_BAR_SIZE } from '../constants';
export default {
computed: {
/**
* Return section width after reducing scrollbar size
* based on listScrollable such that Epic item cells
* do not consider scrollbar presence in shellWidth
*/
sectionShellWidth() {
return this.shellWidth - (this.listScrollable ? SCROLL_BAR_SIZE : 0);
},
sectionItemWidth() {
const timeframeLength = this.timeframe.length;
// Calculate minimum width for single cell
// based on total number of months in current timeframe
// and available shellWidth
const width = (this.sectionShellWidth - EPIC_DETAILS_CELL_WIDTH) / timeframeLength;
// When shellWidth is too low, we need to obey global
// minimum cell width.
return Math.max(width, TIMELINE_CELL_MIN_WIDTH);
},
sectionContainerStyles() {
const width = EPIC_DETAILS_CELL_WIDTH + this.sectionItemWidth * this.timeframe.length;
return {
width: `${width}px`,
};
},
},
};
...@@ -11,8 +11,8 @@ export default { ...@@ -11,8 +11,8 @@ export default {
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6); lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
return ( return (
this.epic.startDate.getTime() >= firstDayOfWeek.getTime() && this.epicStartDateValues.time >= firstDayOfWeek.getTime() &&
this.epic.startDate.getTime() <= lastDayOfWeek.getTime() this.epicStartDateValues.time <= lastDayOfWeek.getTime()
); );
}, },
/** /**
...@@ -26,9 +26,9 @@ export default { ...@@ -26,9 +26,9 @@ export default {
/** /**
* Check if current epic ends within current week (timeline cell) * Check if current epic ends within current week (timeline cell)
*/ */
isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate) { isTimeframeUnderEndDateForWeek(timeframeItem) {
const lastDayOfWeek = this.getLastDayOfWeek(timeframeItem); const lastDayOfWeek = this.getLastDayOfWeek(timeframeItem);
return epicEndDate.getTime() <= lastDayOfWeek.getTime(); return this.epicEndDateValues.time <= lastDayOfWeek.getTime();
}, },
/** /**
* Return timeline bar width for current week (timeline cell) based on * Return timeline bar width for current week (timeline cell) based on
...@@ -55,8 +55,8 @@ export default { ...@@ -55,8 +55,8 @@ export default {
*/ */
getTimelineBarStartOffsetForWeeks() { getTimelineBarStartOffsetForWeeks() {
const daysInWeek = 7; const daysInWeek = 7;
const dayWidth = this.getCellWidth() / daysInWeek; const dayWidth = this.$options.cellWidth / daysInWeek;
const startDate = this.epic.startDate.getDay() + 1; const startDate = this.epicStartDateValues.day + 1;
const firstDayOfWeek = this.timeframeItem.getDay() + 1; const firstDayOfWeek = this.timeframeItem.getDay() + 1;
if ( if (
...@@ -98,24 +98,24 @@ export default { ...@@ -98,24 +98,24 @@ export default {
let timelineBarWidth = 0; let timelineBarWidth = 0;
const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem); const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth(); const { cellWidth } = this.$options;
const epicStartDate = this.epic.startDate; const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epic.endDate; const epicEndDate = this.epicEndDateValues;
for (let i = indexOfCurrentWeek; i < this.timeframe.length; i += 1) { for (let i = indexOfCurrentWeek; i < this.timeframe.length; i += 1) {
if (i === indexOfCurrentWeek) { if (i === indexOfCurrentWeek) {
if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) { if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek( timelineBarWidth += this.getBarWidthForSingleWeek(
cellWidth, cellWidth,
epicEndDate.getDay() - epicStartDate.getDay() + 1, epicEndDate.day - epicStartDate.day + 1,
); );
break; break;
} else { } else {
const date = epicStartDate.getDay() === 0 ? 7 : 7 - epicStartDate.getDay(); const date = epicStartDate.day === 0 ? 7 : 7 - epicStartDate.day;
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, date); timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, date);
} }
} else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) { } else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.getDay() + 1); timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.day + 1);
break; break;
} else { } else {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, 7); timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, 7);
......
...@@ -84,7 +84,7 @@ export const receiveEpicsSuccess = ( ...@@ -84,7 +84,7 @@ export const receiveEpicsSuccess = (
// Exclude any Epic that has invalid dates // Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline // or is already present in Roadmap timeline
if ( if (
formattedEpic.startDate <= formattedEpic.endDate && formattedEpic.startDate.getTime() <= formattedEpic.endDate.getTime() &&
state.epicIds.indexOf(formattedEpic.id) < 0 state.epicIds.indexOf(formattedEpic.id) < 0
) { ) {
Object.assign(formattedEpic, { Object.assign(formattedEpic, {
...@@ -192,5 +192,7 @@ export const refreshEpicDates = ({ commit, state, getters }) => { ...@@ -192,5 +192,7 @@ export const refreshEpicDates = ({ commit, state, getters }) => {
commit(types.SET_EPICS, epics); commit(types.SET_EPICS, epics);
}; };
export const setBufferSize = ({ commit }, bufferSize) => commit(types.SET_BUFFER_SIZE, bufferSize);
// 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 () => {};
...@@ -15,3 +15,5 @@ export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE'; ...@@ -15,3 +15,5 @@ export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const PREPEND_TIMEFRAME = 'PREPEND_TIMEFRAME'; export const PREPEND_TIMEFRAME = 'PREPEND_TIMEFRAME';
export const APPEND_TIMEFRAME = 'APPEND_TIMEFRAME'; export const APPEND_TIMEFRAME = 'APPEND_TIMEFRAME';
export const SET_BUFFER_SIZE = 'SET_BUFFER_SIZE';
...@@ -50,4 +50,8 @@ export default { ...@@ -50,4 +50,8 @@ export default {
state.extendedTimeframe = extendedTimeframe; state.extendedTimeframe = extendedTimeframe;
state.timeframe.push(...extendedTimeframe); state.timeframe.push(...extendedTimeframe);
}, },
[types.SET_BUFFER_SIZE](state, bufferSize) {
state.bufferSize = bufferSize;
},
}; };
...@@ -9,6 +9,7 @@ export default () => ({ ...@@ -9,6 +9,7 @@ export default () => ({
// Data // Data
epicIid: '', epicIid: '',
epics: [], epics: [],
visibleEpics: [],
epicIds: [], epicIds: [],
currentGroupId: -1, currentGroupId: -1,
fullPath: '', fullPath: '',
...@@ -16,6 +17,7 @@ export default () => ({ ...@@ -16,6 +17,7 @@ export default () => ({
extendedTimeframe: [], extendedTimeframe: [],
presetType: '', presetType: '',
sortedBy: '', sortedBy: '',
bufferSize: 0,
// UI Flags // UI Flags
defaultInnerHeight: 0, defaultInnerHeight: 0,
......
...@@ -19,30 +19,43 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -19,30 +19,43 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
} }
@keyframes fadeInDetails { @mixin roadmap-scroll-mixin {
from { height: $grid-size;
opacity: 0; width: $details-cell-width;
} pointer-events: none;
}
to { html.group-epics-roadmap-html {
opacity: 1; height: 100%;
// We need to reset this just for Roadmap page
overflow-y: initial;
}
.with-performance-bar {
.group-epics-roadmap-body {
$header-size: $performance-bar-height + $header-height;
height: calc(100% - #{$header-size});
} }
} }
@keyframes fadeinTimelineBar { .group-epics-roadmap-body {
from { height: calc(100% - #{$header-height});
opacity: 0;
.page-with-contextual-sidebar {
height: 100%;
} }
to { .group-epics-roadmap {
opacity: 0.75; // This size is total of breadcrumb height and computed height of
// filters container (70px)
$header-size: $breadcrumb-min-height + 70px;
height: calc(100% - #{$header-size});
} }
}
@mixin roadmap-scroll-mixin { .group-epics-roadmap-wrapper,
height: $grid-size; .group-epics-roadmap .content {
width: $details-cell-width; height: 100%;
pointer-events: none; }
} }
.epics-details-filters { .epics-details-filters {
...@@ -100,6 +113,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -100,6 +113,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-container { .roadmap-container {
overflow: hidden; overflow: hidden;
height: 100%;
&.overflow-reset { &.overflow-reset {
overflow: initial; overflow: initial;
...@@ -108,12 +122,13 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -108,12 +122,13 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-shell { .roadmap-shell {
position: relative; position: relative;
height: 100%;
width: 100%;
overflow-x: auto; overflow-x: auto;
.skeleton-loader { .skeleton-loader {
position: absolute; position: absolute;
top: $header-item-height; top: $header-item-height;
left: $timeline-cell-width / 2;
width: $details-cell-width; width: $details-cell-width;
height: 100%; height: 100%;
padding-top: $gl-padding-top; padding-top: $gl-padding-top;
...@@ -171,6 +186,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -171,6 +186,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
.timeline-header-item { .timeline-header-item {
width: $timeline-cell-width;
&:last-of-type .item-label { &:last-of-type .item-label {
border-right: 0; border-right: 0;
} }
...@@ -241,6 +258,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -241,6 +258,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
.epics-list-section { .epics-list-section {
height: calc(100% - 60px);
.epics-list-item { .epics-list-item {
&:hover { &:hover {
.epic-details-cell, .epic-details-cell,
...@@ -250,6 +269,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -250,6 +269,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
&.epics-list-item-empty { &.epics-list-item-empty {
height: 100%;
&:hover { &:hover {
.epic-details-cell, .epic-details-cell,
.epic-timeline-cell { .epic-timeline-cell {
...@@ -295,7 +316,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -295,7 +316,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.epic-title, .epic-title,
.epic-group-timeframe { .epic-group-timeframe {
animation: fadeInDetails 1s; will-change: contents;
} }
.epic-title { .epic-title {
...@@ -327,6 +348,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -327,6 +348,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
.epic-timeline-cell { .epic-timeline-cell {
width: $timeline-cell-width;
background-color: transparent; background-color: transparent;
border-right: $border-style; border-right: $border-style;
...@@ -341,7 +363,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -341,7 +363,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
background-color: $blue-500; background-color: $blue-500;
border-radius: $border-radius-default; border-radius: $border-radius-default;
opacity: 0.75; opacity: 0.75;
animation: fadeinTimelineBar 1s; will-change: width, left;
&:hover { &:hover {
opacity: 1; opacity: 1;
......
...@@ -12,6 +12,7 @@ module Groups ...@@ -12,6 +12,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(:roadmap_graphql, @group) push_frontend_feature_flag(:roadmap_graphql, @group)
push_frontend_feature_flag(:roadmap_buffered_rendering, @group)
end end
# show roadmap for a group # show roadmap for a group
......
- @no_breadcrumb_container = true - @no_breadcrumb_container = true
- @no_container = true - @no_container = true
- @html_class = "group-epics-roadmap-html"
- @body_class = "group-epics-roadmap-body"
- @content_wrapper_class = "group-epics-roadmap-wrapper" - @content_wrapper_class = "group-epics-roadmap-wrapper"
- @content_class = "group-epics-roadmap" - @content_class = "group-epics-roadmap"
- breadcrumb_title _("Epics Roadmap") - breadcrumb_title _("Epics Roadmap")
......
...@@ -76,18 +76,6 @@ describe 'group epic roadmap', :js do ...@@ -76,18 +76,6 @@ describe 'group epic roadmap', :js do
expect(page).to have_selector('.epics-list-item .epic-title', count: 3) expect(page).to have_selector('.epics-list-item .epic-title', count: 3)
end end
end end
it 'resizing browser window causes Roadmap to re-render' do
page.within('.group-epics-roadmap .roadmap-container') do
initial_style = find('.roadmap-shell')[:style]
page.current_window.resize_to(2500, 1000)
wait_for_requests
expect(find('.roadmap-shell')[:style]).not_to eq(initial_style)
restore_window_size
end
end
end end
describe 'roadmap page with epics state filter' do describe 'roadmap page with epics state filter' do
......
...@@ -140,7 +140,7 @@ describe('EpicItemDetailsComponent', () => { ...@@ -140,7 +140,7 @@ describe('EpicItemDetailsComponent', () => {
expect(epicGroupNameEl).not.toBeNull(); expect(epicGroupNameEl).not.toBeNull();
expect(epicGroupNameEl.innerText.trim()).toContain(mockEpicItem.groupName); expect(epicGroupNameEl.innerText.trim()).toContain(mockEpicItem.groupName);
expect(epicGroupNameEl.dataset.originalTitle).toBe(mockEpicItem.groupFullName); expect(epicGroupNameEl.getAttribute('title')).toBe(mockEpicItem.groupFullName);
}); });
it('renders Epic timeframe', () => { it('renders Epic timeframe', () => {
......
...@@ -9,13 +9,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -9,13 +9,7 @@ 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 { mockTimeframeInitialDate, mockEpic, mockGroupId } from '../mock_data';
mockTimeframeInitialDate,
mockEpic,
mockGroupId,
mockShellWidth,
mockItemWidth,
} from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -24,8 +18,6 @@ const createComponent = ({ ...@@ -24,8 +18,6 @@ const createComponent = ({
epic = mockEpic, epic = mockEpic,
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId, currentGroupId = mockGroupId,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(epicItemComponent); const Component = Vue.extend(epicItemComponent);
...@@ -34,8 +26,6 @@ const createComponent = ({ ...@@ -34,8 +26,6 @@ const createComponent = ({
epic, epic,
timeframe, timeframe,
currentGroupId, currentGroupId,
shellWidth,
itemWidth,
}); });
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import epicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue'; import epicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import eventHub from 'ee/roadmap/event_hub';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { TIMELINE_CELL_MIN_WIDTH, 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data'; import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -16,8 +15,6 @@ const createComponent = ({ ...@@ -16,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0], timeframeItem = mockTimeframeMonths[0],
epic = mockEpic, epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(epicItemTimelineComponent); const Component = Vue.extend(epicItemTimelineComponent);
...@@ -26,8 +23,6 @@ const createComponent = ({ ...@@ -26,8 +23,6 @@ const createComponent = ({
timeframe, timeframe,
timeframeItem, timeframeItem,
epic, epic,
shellWidth,
itemWidth,
}); });
}; };
...@@ -42,80 +37,25 @@ describe('EpicItemTimelineComponent', () => { ...@@ -42,80 +37,25 @@ describe('EpicItemTimelineComponent', () => {
it('returns default data props', () => { it('returns default data props', () => {
vm = createComponent({}); vm = createComponent({});
expect(vm.timelineBarReady).toBe(false); expect(vm.epicStartDateValues).toEqual(
expect(vm.timelineBarStyles).toBe(''); jasmine.objectContaining({
}); day: mockEpic.startDate.getDay(),
}); date: mockEpic.startDate.getDate(),
month: mockEpic.startDate.getMonth(),
describe('computed', () => { year: mockEpic.startDate.getFullYear(),
describe('itemStyles', () => { time: mockEpic.startDate.getTime(),
it('returns CSS min-width based on getCellWidth() method', () => { }),
vm = createComponent({}); );
expect(vm.itemStyles.width).toBe(`${mockItemWidth}px`); expect(vm.epicEndDateValues).toEqual(
}); jasmine.objectContaining({
}); day: mockEpic.endDate.getDay(),
}); date: mockEpic.endDate.getDate(),
month: mockEpic.endDate.getMonth(),
describe('methods', () => { year: mockEpic.endDate.getFullYear(),
describe('getCellWidth', () => { time: mockEpic.endDate.getTime(),
it('returns proportionate width based on timeframe length and shellWidth', () => { }),
vm = createComponent({}); );
expect(vm.getCellWidth()).toBe(210);
});
it('returns minimum fixed width when proportionate width available lower than minimum fixed width defined', () => {
vm = createComponent({
shellWidth: 1000,
});
expect(vm.getCellWidth()).toBe(TIMELINE_CELL_MIN_WIDTH);
});
});
describe('renderTimelineBar', () => {
it('sets `timelineBarStyles` & `timelineBarReady` when timeframeItem has Epic.startDate', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('width: 1274px; left: 0;');
expect(vm.timelineBarReady).toBe(true);
});
it('does not set `timelineBarStyles` & `timelineBarReady` when timeframeItem does NOT have Epic.startDate', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[0] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('');
expect(vm.timelineBarReady).toBe(false);
});
});
});
describe('mounted', () => {
it('binds `refreshTimeline` event listener on eventHub', () => {
spyOn(eventHub, '$on');
const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds `refreshTimeline` event listener on eventHub', () => {
spyOn(eventHub, '$off');
const vmX = createComponent({});
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
}); });
}); });
...@@ -126,12 +66,6 @@ describe('EpicItemTimelineComponent', () => { ...@@ -126,12 +66,6 @@ describe('EpicItemTimelineComponent', () => {
expect(vm.$el.classList.contains('epic-timeline-cell')).toBe(true); expect(vm.$el.classList.contains('epic-timeline-cell')).toBe(true);
}); });
it('renders component container element with `min-width` property applied via style attribute', () => {
vm = createComponent({});
expect(vm.$el.getAttribute('style')).toBe(`width: ${mockItemWidth}px;`);
});
it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => { it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => {
vm = createComponent({ vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }), epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
...@@ -141,7 +75,7 @@ describe('EpicItemTimelineComponent', () => { ...@@ -141,7 +75,7 @@ describe('EpicItemTimelineComponent', () => {
expect(vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar')).not.toBeNull(); expect(vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar')).not.toBeNull();
}); });
it('renders timeline bar with calculated `width` and `left` properties applied via style attribute', done => { it('renders timeline bar with calculated `width` and `left` properties applied via style attribute', () => {
vm = createComponent({ vm = createComponent({
epic: Object.assign({}, mockEpic, { epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeMonths[0], startDate: mockTimeframeMonths[0],
...@@ -150,11 +84,8 @@ describe('EpicItemTimelineComponent', () => { ...@@ -150,11 +84,8 @@ describe('EpicItemTimelineComponent', () => {
}); });
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar'); const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar(); expect(timelineBarEl.getAttribute('style')).toContain('width');
vm.$nextTick(() => { expect(timelineBarEl.getAttribute('style')).toContain('left: 0px;');
expect(timelineBarEl.getAttribute('style')).toBe('width: 742.5px; left: 0px;');
done();
});
}); });
it('renders timeline bar with `start-date-undefined` class when Epic startDate is undefined', done => { it('renders timeline bar with `start-date-undefined` class when Epic startDate is undefined', done => {
...@@ -166,7 +97,6 @@ describe('EpicItemTimelineComponent', () => { ...@@ -166,7 +97,6 @@ describe('EpicItemTimelineComponent', () => {
}); });
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar'); const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => { vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('start-date-undefined')).toBe(true); expect(timelineBarEl.classList.contains('start-date-undefined')).toBe(true);
done(); done();
...@@ -183,7 +113,6 @@ describe('EpicItemTimelineComponent', () => { ...@@ -183,7 +113,6 @@ describe('EpicItemTimelineComponent', () => {
}); });
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar'); const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => { vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('end-date-undefined')).toBe(true); expect(timelineBarEl.classList.contains('end-date-undefined')).toBe(true);
done(); done();
......
import Vue from 'vue'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue'; import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
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 mountComponent from 'spec/helpers/vue_mount_component_helper';
import { import {
mockShellWidth, PRESET_TYPES,
EPIC_DETAILS_CELL_WIDTH,
TIMELINE_CELL_MIN_WIDTH,
} from 'ee/roadmap/constants';
import {
mockTimeframeInitialDate, mockTimeframeInitialDate,
mockGroupId, mockGroupId,
rawEpics, rawEpics,
...@@ -17,7 +18,6 @@ import { ...@@ -17,7 +18,6 @@ import {
} from '../mock_data'; } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = createStore(); const store = createStore();
store.dispatch('setInitialData', { store.dispatch('setInitialData', {
currentGroupId: mockGroupId, currentGroupId: mockGroupId,
...@@ -37,147 +37,214 @@ const createComponent = ({ ...@@ -37,147 +37,214 @@ const createComponent = ({
epics = mockEpics, epics = mockEpics,
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId, currentGroupId = mockGroupId,
shellWidth = mockShellWidth,
listScrollable = false,
presetType = PRESET_TYPES.MONTHS, presetType = PRESET_TYPES.MONTHS,
}) => { roadmapBufferedRendering = true,
const Component = Vue.extend(epicsListSectionComponent); } = {}) => {
const localVue = createLocalVue();
return mountComponent(Component, {
return shallowMount(epicsListSectionComponent, {
localVue,
store,
stubs: {
'epic-item': false,
'virtual-list': false,
},
propsData: {
presetType, presetType,
epics, epics,
timeframe, timeframe,
currentGroupId, currentGroupId,
shellWidth, },
listScrollable, provide: {
glFeatures: { roadmapBufferedRendering },
},
}); });
}; };
describe('EpicsListSectionComponent', () => { describe('EpicsListSectionComponent', () => {
let vm; let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
vm = createComponent({}); expect(wrapper.vm.offsetLeft).toBe(0);
expect(wrapper.vm.emptyRowContainerStyles).toEqual({});
expect(vm.shellHeight).toBe(0); expect(wrapper.vm.showBottomShadow).toBe(false);
expect(vm.emptyRowHeight).toBe(0); expect(wrapper.vm.roadmapShellEl).toBeDefined();
expect(vm.showEmptyRow).toBe(false);
expect(vm.offsetLeft).toBe(0);
expect(vm.showBottomShadow).toBe(false);
}); });
}); });
describe('computed', () => { describe('computed', () => {
beforeEach(() => { describe('emptyRowContainerVisible', () => {
vm = createComponent({}); it('returns true when total epics are less than buffer size', () => {
}); wrapper.vm.setBufferSize(wrapper.vm.epics.length + 1);
describe('emptyRowContainerStyles', () => { expect(wrapper.vm.emptyRowContainerVisible).toBe(true);
it('returns computed style object based on emptyRowHeight prop value', () => {
expect(vm.emptyRowContainerStyles.height).toBe('0px');
}); });
}); });
describe('emptyRowCellStyles', () => { describe('sectionContainerStyles', () => {
it('returns computed style object based on sectionItemWidth prop value', () => { it('returns style string for container element based on sectionShellWidth', () => {
expect(vm.emptyRowCellStyles.width).toBe('210px'); expect(wrapper.vm.sectionContainerStyles.width).toBe(
`${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * wrapper.vm.timeframe.length}px`,
);
}); });
}); });
describe('shadowCellStyles', () => { describe('shadowCellStyles', () => {
it('returns computed style object based on `offsetLeft` prop value', () => { it('returns computed style object based on `offsetLeft` prop value', () => {
expect(vm.shadowCellStyles.left).toBe('0px'); expect(wrapper.vm.shadowCellStyles.left).toBe('0px');
}); });
}); });
}); });
describe('methods', () => { describe('methods', () => {
beforeEach(() => { describe('initMounted', () => {
vm = createComponent({}); it('sets value of `roadmapShellEl` with root component element', () => {
expect(wrapper.vm.roadmapShellEl instanceof HTMLElement).toBe(true);
}); });
describe('initMounted', () => { it('calls action `setBufferSize` with value based on window.innerHeight and component element position', () => {
it('initializes shellHeight based on window.innerHeight and component element position', done => { expect(wrapper.vm.bufferSize).toBe(12);
vm.$nextTick(() => { });
expect(vm.shellHeight).toBe(600);
it('sets value of `offsetLeft` with parentElement.offsetLeft', done => {
wrapper.vm.$nextTick(() => {
// During tests, there's no `$el.parentElement` present
// hence offsetLeft is 0.
expect(wrapper.vm.offsetLeft).toBe(0);
done(); done();
}); });
}); });
it('calls initEmptyRow() when there are Epics to render', done => { it('calls `scrollToTodayIndicator` following the component render', done => {
spyOn(vm, 'initEmptyRow').and.callThrough(); spyOn(wrapper.vm, 'scrollToTodayIndicator');
vm.$nextTick(() => { // Original method implementation waits for render cycle
expect(vm.initEmptyRow).toHaveBeenCalled(); // to complete at 2 levels before scrolling.
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.scrollToTodayIndicator).toHaveBeenCalled();
done(); done();
}); });
}); });
});
it('emits `epicsListRendered` via eventHub', done => { it('sets style object to `emptyRowContainerStyles`', done => {
spyOn(eventHub, '$emit'); wrapper.vm.$nextTick(() => {
expect(wrapper.vm.emptyRowContainerStyles).toEqual(
vm.$nextTick(() => { jasmine.objectContaining({
expect(eventHub.$emit).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Object)); height: '0px',
}),
);
done(); done();
}); });
}); });
}); });
describe('initEmptyRow', () => { describe('getEmptyRowContainerStyles', () => {
it('sets `emptyRowHeight` and `showEmptyRow` props when shellHeight is greater than approximate height of epics list', done => { it('returns empty object when there are no epics available to render', () => {
vm.$nextTick(() => { wrapper.setProps({
expect(vm.emptyRowHeight).toBe(600); epics: [],
expect(vm.showEmptyRow).toBe(true);
done();
}); });
expect(wrapper.vm.getEmptyRowContainerStyles()).toEqual({});
}); });
it('does not set `emptyRowHeight` and `showEmptyRow` props when shellHeight is less than approximate height of epics list', done => { it('returns object containing `height` when there epics available to render', () => {
const initialHeight = window.innerHeight; expect(wrapper.vm.getEmptyRowContainerStyles()).toEqual(
window.innerHeight = 0; jasmine.objectContaining({
const vmMoreEpics = createComponent({ height: '0px',
epics: mockEpics.concat(mockEpics).concat(mockEpics), }),
);
}); });
vmMoreEpics.$nextTick(() => {
expect(vmMoreEpics.emptyRowHeight).toBe(0);
expect(vmMoreEpics.showEmptyRow).toBe(false);
window.innerHeight = initialHeight; // reset to prevent any side effects
done();
}); });
describe('handleEpicsListScroll', () => {
it('toggles value of `showBottomShadow` based on provided `scrollTop`, `clientHeight` & `scrollHeight`', () => {
wrapper.vm.handleEpicsListScroll({
scrollTop: 5,
clientHeight: 5,
scrollHeight: 15,
});
// Math.ceil(scrollTop) + clientHeight < scrollHeight
expect(wrapper.vm.showBottomShadow).toBe(true);
wrapper.vm.handleEpicsListScroll({
scrollTop: 15,
clientHeight: 5,
scrollHeight: 15,
});
// Math.ceil(scrollTop) + clientHeight < scrollHeight
expect(wrapper.vm.showBottomShadow).toBe(false);
});
});
describe('getEpicItemProps', () => {
it('returns an object containing props for EpicItem component', () => {
expect(wrapper.vm.getEpicItemProps(1)).toEqual(
jasmine.objectContaining({
key: 1,
props: {
epic: wrapper.vm.epics[1],
presetType: wrapper.vm.presetType,
timeframe: wrapper.vm.timeframe,
currentGroupId: wrapper.vm.currentGroupId,
},
}),
);
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
beforeEach(() => { it('renders component container element with class `epics-list-section`', () => {
vm = createComponent({}); expect(wrapper.vm.$el.classList.contains('epics-list-section')).toBe(true);
}); });
it('renders component container element with class `epics-list-section`', done => { it('renders virtual-list when roadmapBufferedRendering is `true` and `epics.length` is more than `bufferSize`', () => {
vm.$nextTick(() => { wrapper.vm.setBufferSize(5);
expect(vm.$el.classList.contains('epics-list-section')).toBe(true);
done(); expect(wrapper.find('virtuallist-stub').exists()).toBe(true);
}); });
it('renders epic-item when roadmapBufferedRendering is `false`', () => {
const wrapperFlagOff = createComponent({
roadmapBufferedRendering: false,
}); });
it('renders component container element with `width` property applied via style attribute', done => { expect(wrapperFlagOff.find('epicitem-stub').exists()).toBe(true);
vm.$nextTick(() => {
expect(vm.$el.getAttribute('style')).toBe(`width: ${mockShellWidth}px;`); wrapperFlagOff.destroy();
done(); });
it('renders epic-item when roadmapBufferedRendering is `true` and `epics.length` is less than `bufferSize`', () => {
wrapper.vm.setBufferSize(50);
expect(wrapper.find('epicitem-stub').exists()).toBe(true);
}); });
it('renders empty row element when `epics.length` is less than `bufferSize`', () => {
wrapper.vm.setBufferSize(50);
expect(wrapper.find('.epics-list-item-empty').exists()).toBe(true);
}); });
it('renders bottom shadow element when `showBottomShadow` prop is true', done => { it('renders bottom shadow element when `showBottomShadow` prop is true', () => {
vm.showBottomShadow = true; wrapper.setData({
vm.$nextTick(() => { showBottomShadow: true,
expect(vm.$el.querySelector('.scroll-bottom-shadow')).not.toBe(null);
done();
}); });
expect(wrapper.find('.scroll-bottom-shadow').exists()).toBe(true);
}); });
}); });
}); });
...@@ -4,7 +4,7 @@ import MonthsHeaderItemComponent from 'ee/roadmap/components/preset_months/month ...@@ -4,7 +4,7 @@ import MonthsHeaderItemComponent from 'ee/roadmap/components/preset_months/month
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeIndex = 0; const mockTimeframeIndex = 0;
...@@ -13,8 +13,6 @@ const createComponent = ({ ...@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex, timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeMonths[mockTimeframeIndex], timeframeItem = mockTimeframeMonths[mockTimeframeIndex],
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(MonthsHeaderItemComponent); const Component = Vue.extend(MonthsHeaderItemComponent);
...@@ -22,8 +20,6 @@ const createComponent = ({ ...@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex, timeframeIndex,
timeframeItem, timeframeItem,
timeframe, timeframe,
shellWidth,
itemWidth,
}); });
}; };
...@@ -46,14 +42,6 @@ describe('MonthsHeaderItemComponent', () => { ...@@ -46,14 +42,6 @@ describe('MonthsHeaderItemComponent', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('timelineHeaderLabel', () => { describe('timelineHeaderLabel', () => {
it('returns string containing Year and Month for current timeline header item', () => { it('returns string containing Year and Month for current timeline header item', () => {
vm = createComponent({}); vm = createComponent({});
......
...@@ -4,7 +4,7 @@ import QuartersHeaderItemComponent from 'ee/roadmap/components/preset_quarters/q ...@@ -4,7 +4,7 @@ import QuartersHeaderItemComponent from 'ee/roadmap/components/preset_quarters/q
import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeIndex = 0; const mockTimeframeIndex = 0;
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate); const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
...@@ -13,8 +13,6 @@ const createComponent = ({ ...@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex, timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeQuarters[mockTimeframeIndex], timeframeItem = mockTimeframeQuarters[mockTimeframeIndex],
timeframe = mockTimeframeQuarters, timeframe = mockTimeframeQuarters,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(QuartersHeaderItemComponent); const Component = Vue.extend(QuartersHeaderItemComponent);
...@@ -22,8 +20,6 @@ const createComponent = ({ ...@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex, timeframeIndex,
timeframeItem, timeframeItem,
timeframe, timeframe,
shellWidth,
itemWidth,
}); });
}; };
...@@ -44,14 +40,6 @@ describe('QuartersHeaderItemComponent', () => { ...@@ -44,14 +40,6 @@ describe('QuartersHeaderItemComponent', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('quarterBeginDate', () => { describe('quarterBeginDate', () => {
it('returns date object representing quarter begin date for current `timeframeItem`', () => { it('returns date object representing quarter begin date for current `timeframeItem`', () => {
expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]); expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]);
......
...@@ -4,7 +4,7 @@ import WeeksHeaderItemComponent from 'ee/roadmap/components/preset_weeks/weeks_h ...@@ -4,7 +4,7 @@ import WeeksHeaderItemComponent from 'ee/roadmap/components/preset_weeks/weeks_h
import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
const mockTimeframeIndex = 0; const mockTimeframeIndex = 0;
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
...@@ -13,8 +13,6 @@ const createComponent = ({ ...@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex, timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeWeeks[mockTimeframeIndex], timeframeItem = mockTimeframeWeeks[mockTimeframeIndex],
timeframe = mockTimeframeWeeks, timeframe = mockTimeframeWeeks,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(WeeksHeaderItemComponent); const Component = Vue.extend(WeeksHeaderItemComponent);
...@@ -22,8 +20,6 @@ const createComponent = ({ ...@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex, timeframeIndex,
timeframeItem, timeframeItem,
timeframe, timeframe,
shellWidth,
itemWidth,
}); });
}; };
...@@ -44,14 +40,6 @@ describe('WeeksHeaderItemComponent', () => { ...@@ -44,14 +40,6 @@ describe('WeeksHeaderItemComponent', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('itemStyles', () => {
it('returns style object for container element based on value of `itemWidth` prop', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe('180px');
});
});
describe('lastDayOfCurrentWeek', () => { describe('lastDayOfCurrentWeek', () => {
it('returns date object representing last day of the week as set in `timeframeItem`', () => { it('returns date object representing last day of the week as set in `timeframeItem`', () => {
expect(vm.lastDayOfCurrentWeek.getDate()).toBe( expect(vm.lastDayOfCurrentWeek.getDate()).toBe(
......
...@@ -59,10 +59,6 @@ describe('Roadmap AppComponent', () => { ...@@ -59,10 +59,6 @@ describe('Roadmap AppComponent', () => {
}); });
describe('data', () => { describe('data', () => {
it('returns default data props', () => {
expect(vm.handleResizeThrottled).toBeDefined();
});
describe('when `gon.feature.roadmapGraphql` is true', () => { describe('when `gon.feature.roadmapGraphql` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features); const originalGonFeatures = Object.assign({}, gon.features);
...@@ -252,35 +248,6 @@ describe('Roadmap AppComponent', () => { ...@@ -252,35 +248,6 @@ describe('Roadmap AppComponent', () => {
}); });
}); });
describe('mounted', () => {
it('binds window resize event listener', () => {
spyOn(window, 'addEventListener');
const vmX = createComponent();
expect(vmX.handleResizeThrottled).toBeDefined();
expect(window.addEventListener).toHaveBeenCalledWith(
'resize',
vmX.handleResizeThrottled,
false,
);
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds window resize event listener', () => {
spyOn(window, 'removeEventListener');
const vmX = createComponent();
vmX.$destroy();
expect(window.removeEventListener).toHaveBeenCalledWith(
'resize',
vmX.handleResizeThrottled,
false,
);
});
});
describe('template', () => { describe('template', () => {
it('renders roadmap container with class `roadmap-container`', () => { it('renders roadmap container with class `roadmap-container`', () => {
expect(vm.$el.classList.contains('roadmap-container')).toBe(true); expect(vm.$el.classList.contains('roadmap-container')).toBe(true);
......
...@@ -43,8 +43,9 @@ const createComponent = ( ...@@ -43,8 +43,9 @@ const createComponent = (
describe('RoadmapShellComponent', () => { describe('RoadmapShellComponent', () => {
let vm; let vm;
beforeEach(() => { beforeEach(done => {
vm = createComponent({}); vm = createComponent({});
vm.$nextTick(done);
}); });
afterEach(() => { afterEach(() => {
...@@ -53,39 +54,10 @@ describe('RoadmapShellComponent', () => { ...@@ -53,39 +54,10 @@ describe('RoadmapShellComponent', () => {
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
expect(vm.shellWidth).toBe(0);
expect(vm.shellHeight).toBe(0);
expect(vm.noScroll).toBe(false);
expect(vm.timeframeStartOffset).toBe(0); expect(vm.timeframeStartOffset).toBe(0);
}); });
}); });
describe('computed', () => {
describe('containerStyles', () => {
beforeEach(() => {
document.body.innerHTML +=
'<div class="roadmap-container"><div id="roadmap-shell"></div></div>';
});
afterEach(() => {
document.querySelector('.roadmap-container').remove();
});
it('returns style object based on shellWidth and shellHeight', done => {
const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell'));
Vue.nextTick(() => {
const stylesObj = vmWithParentEl.containerStyles;
// Ensure that value for `width` & `height`
// is a non-zero number.
expect(parseInt(stylesObj.width, 10)).not.toBe(0);
expect(parseInt(stylesObj.height, 10)).not.toBe(0);
vmWithParentEl.$destroy();
done();
});
});
});
});
describe('methods', () => { describe('methods', () => {
beforeEach(() => { beforeEach(() => {
document.body.innerHTML += document.body.innerHTML +=
...@@ -103,7 +75,6 @@ describe('RoadmapShellComponent', () => { ...@@ -103,7 +75,6 @@ describe('RoadmapShellComponent', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
vmWithParentEl.noScroll = false;
vmWithParentEl.handleScroll(); vmWithParentEl.handleScroll();
expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object)); expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object));
...@@ -131,13 +102,5 @@ describe('RoadmapShellComponent', () => { ...@@ -131,13 +102,5 @@ describe('RoadmapShellComponent', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('adds `prevent-vertical-scroll` class on component container element', done => {
vm.noScroll = true;
Vue.nextTick(() => {
expect(vm.$el.classList.contains('prevent-vertical-scroll')).toBe(true);
done();
});
});
}); });
}); });
...@@ -7,7 +7,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -7,7 +7,7 @@ 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 { mockEpic, mockTimeframeInitialDate, mockShellWidth } from '../mock_data'; import { mockEpic, mockTimeframeInitialDate } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -15,17 +15,13 @@ const createComponent = ({ ...@@ -15,17 +15,13 @@ const createComponent = ({
presetType = PRESET_TYPES.MONTHS, presetType = PRESET_TYPES.MONTHS,
epics = [mockEpic], epics = [mockEpic],
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth, } = {}) => {
listScrollable = false,
}) => {
const Component = Vue.extend(roadmapTimelineSectionComponent); const Component = Vue.extend(roadmapTimelineSectionComponent);
return mountComponent(Component, { return mountComponent(Component, {
presetType, presetType,
epics, epics,
timeframe, timeframe,
shellWidth,
listScrollable,
}); });
}; };
...@@ -33,7 +29,7 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -33,7 +29,7 @@ describe('RoadmapTimelineSectionComponent', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
vm = createComponent({}); vm = createComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -46,6 +42,18 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -46,6 +42,18 @@ describe('RoadmapTimelineSectionComponent', () => {
}); });
}); });
describe('computed', () => {
describe('sectionContainerStyles', () => {
it('returns object containing `width` with value based on epic details cell width, timeline cell width and timeframe length', () => {
expect(vm.sectionContainerStyles).toEqual(
jasmine.objectContaining({
width: '1760px',
}),
);
});
});
});
describe('methods', () => { describe('methods', () => {
describe('handleEpicsListScroll', () => { describe('handleEpicsListScroll', () => {
it('sets `scrolled-ahead` class on thead element based on provided scrollTop value', () => { it('sets `scrolled-ahead` class on thead element based on provided scrollTop value', () => {
......
...@@ -42,62 +42,47 @@ describe('TimelineTodayIndicatorComponent', () => { ...@@ -42,62 +42,47 @@ describe('TimelineTodayIndicatorComponent', () => {
it('returns default data props', () => { it('returns default data props', () => {
vm = createComponent({}); vm = createComponent({});
expect(vm.todayBarStyles).toBe(''); expect(vm.todayBarStyles).toEqual({});
expect(vm.todayBarReady).toBe(false); expect(vm.todayBarReady).toBe(true);
}); });
}); });
describe('methods', () => { describe('methods', () => {
describe('handleEpicsListRender', () => { describe('getTodayBarStyles', () => {
it('sets `todayBarStyles` and `todayBarReady` props', () => { it('sets `todayBarStyles` and `todayBarReady` props', () => {
vm = createComponent({}); vm = createComponent({});
vm.handleEpicsListRender({});
const stylesObj = vm.todayBarStyles;
expect(stylesObj.height).toBe('600px');
expect(stylesObj.left).toBe('50%');
expect(vm.todayBarReady).toBe(true);
});
it('sets `todayBarReady` prop based on value of provided `todayBarReady` param', () => { const stylesObj = vm.getTodayBarStyles();
vm = createComponent({});
vm.handleEpicsListRender({
todayBarReady: false,
});
expect(vm.todayBarReady).toBe(false); expect(stylesObj.height).toBe('calc(100vh - 16px)');
expect(stylesObj.left).toBe('50%');
}); });
}); });
}); });
describe('mounted', () => { describe('mounted', () => {
it('binds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => { it('binds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$on'); spyOn(eventHub, '$on');
const vmX = createComponent({}); const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
vmX.$destroy(); vmX.$destroy();
}); });
}); });
describe('beforeDestroy', () => { describe('beforeDestroy', () => {
it('unbinds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => { it('unbinds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$off'); spyOn(eventHub, '$off');
const vmX = createComponent({}); const vmX = createComponent({});
vmX.$destroy(); vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
}); });
}); });
describe('template', () => { describe('template', () => {
it('renders component container element with class `today-bar`', done => { it('renders component container element with class `today-bar`', done => {
vm = createComponent({}); vm = createComponent({});
vm.handleEpicsListRender({});
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.classList.contains('today-bar')).toBe(true); expect(vm.$el.classList.contains('today-bar')).toBe(true);
done(); done();
......
...@@ -6,7 +6,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -6,7 +6,7 @@ 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data'; import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -15,8 +15,6 @@ const createComponent = ({ ...@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths, timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0], timeframeItem = mockTimeframeMonths[0],
epic = mockEpic, epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(EpicItemTimelineComponent); const Component = Vue.extend(EpicItemTimelineComponent);
...@@ -25,8 +23,6 @@ const createComponent = ({ ...@@ -25,8 +23,6 @@ const createComponent = ({
timeframe, timeframe,
timeframeItem, timeframeItem,
epic, epic,
shellWidth,
itemWidth,
}); });
}; };
...@@ -59,22 +55,34 @@ describe('MonthsPresetMixin', () => { ...@@ -59,22 +55,34 @@ describe('MonthsPresetMixin', () => {
}); });
describe('isTimeframeUnderEndDateForMonth', () => { describe('isTimeframeUnderEndDateForMonth', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
beforeEach(() => { beforeEach(() => {
vm = createComponent({}); vm = createComponent({});
}); });
it('returns true if provided timeframeItem is under epicEndDate', () => { it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
const epicEndDate = new Date(2018, 0, 26); // Jan 26, 2018 const epicEndDate = new Date(2018, 0, 26); // Jan 26, 2018
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate)).toBe(true); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem)).toBe(true);
}); });
it('returns false if provided timeframeItem is NOT under epicEndDate', () => { it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
const epicEndDate = new Date(2018, 1, 26); // Feb 26, 2018 const epicEndDate = new Date(2018, 1, 26); // Feb 26, 2018
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate)).toBe(false); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForMonth(timeframeItem)).toBe(false);
}); });
}); });
...@@ -132,7 +140,6 @@ describe('MonthsPresetMixin', () => { ...@@ -132,7 +140,6 @@ describe('MonthsPresetMixin', () => {
describe('getTimelineBarWidthForMonths', () => { describe('getTimelineBarWidthForMonths', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => { it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({ vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeMonths[0], timeframeItem: mockTimeframeMonths[0],
epic: Object.assign({}, mockEpic, { epic: Object.assign({}, mockEpic, {
startDate: new Date(2017, 11, 15), // Dec 15, 2017 startDate: new Date(2017, 11, 15), // Dec 15, 2017
...@@ -140,7 +147,7 @@ describe('MonthsPresetMixin', () => { ...@@ -140,7 +147,7 @@ describe('MonthsPresetMixin', () => {
}), }),
}); });
expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(637); expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(546);
}); });
}); });
}); });
......
...@@ -6,7 +6,7 @@ import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -6,7 +6,7 @@ import { getTimeframeForQuartersView } 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data'; import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate); const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
...@@ -15,8 +15,6 @@ const createComponent = ({ ...@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeQuarters, timeframe = mockTimeframeQuarters,
timeframeItem = mockTimeframeQuarters[0], timeframeItem = mockTimeframeQuarters[0],
epic = mockEpic, epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(EpicItemTimelineComponent); const Component = Vue.extend(EpicItemTimelineComponent);
...@@ -25,8 +23,6 @@ const createComponent = ({ ...@@ -25,8 +23,6 @@ const createComponent = ({
timeframe, timeframe,
timeframeItem, timeframeItem,
epic, epic,
shellWidth,
itemWidth,
}); });
}; };
...@@ -59,22 +55,34 @@ describe('QuartersPresetMixin', () => { ...@@ -59,22 +55,34 @@ describe('QuartersPresetMixin', () => {
}); });
describe('isTimeframeUnderEndDateForQuarter', () => { describe('isTimeframeUnderEndDateForQuarter', () => {
const timeframeItem = mockTimeframeQuarters[1];
beforeEach(() => { beforeEach(() => {
vm = createComponent({}); vm = createComponent({});
}); });
it('returns true if provided timeframeItem is under epicEndDate', () => { it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = mockTimeframeQuarters[1];
const epicEndDate = mockTimeframeQuarters[1].range[2]; const epicEndDate = mockTimeframeQuarters[1].range[2];
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate)).toBe(true); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem)).toBe(true);
}); });
it('returns false if provided timeframeItem is NOT under epicEndDate', () => { it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = mockTimeframeQuarters[1];
const epicEndDate = mockTimeframeQuarters[2].range[1]; const epicEndDate = mockTimeframeQuarters[2].range[1];
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate)).toBe(false); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForQuarter(timeframeItem)).toBe(false);
}); });
}); });
...@@ -132,7 +140,6 @@ describe('QuartersPresetMixin', () => { ...@@ -132,7 +140,6 @@ describe('QuartersPresetMixin', () => {
describe('getTimelineBarWidthForQuarters', () => { describe('getTimelineBarWidthForQuarters', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => { it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({ vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeQuarters[0], timeframeItem: mockTimeframeQuarters[0],
epic: Object.assign({}, mockEpic, { epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeQuarters[0].range[1], startDate: mockTimeframeQuarters[0].range[1],
...@@ -140,7 +147,7 @@ describe('QuartersPresetMixin', () => { ...@@ -140,7 +147,7 @@ describe('QuartersPresetMixin', () => {
}), }),
}); });
expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(240); expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(180);
}); });
}); });
}); });
......
import Vue from 'vue';
import roadmapTimelineSectionComponent from 'ee/roadmap/components/roadmap_timeline_section.vue';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockEpic,
mockTimeframeInitialDate,
mockShellWidth,
mockScrollBarSize,
} from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
epics = [mockEpic],
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
listScrollable = false,
}) => {
const Component = Vue.extend(roadmapTimelineSectionComponent);
return mountComponent(Component, {
presetType,
epics,
timeframe,
shellWidth,
listScrollable,
});
};
describe('SectionMixin', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('sectionShellWidth', () => {
it('returns shellWidth as it is when `listScrollable` prop is false', () => {
expect(vm.sectionShellWidth).toBe(mockShellWidth);
});
it('returns shellWidth after deducating value of SCROLL_BAR_SIZE when `listScrollable` prop is true', () => {
const vmScrollable = createComponent({ listScrollable: true });
expect(vmScrollable.sectionShellWidth).toBe(mockShellWidth - mockScrollBarSize);
vmScrollable.$destroy();
});
});
describe('sectionItemWidth', () => {
it('returns calculated item width based on sectionShellWidth and timeframe size', () => {
expect(vm.sectionItemWidth).toBe(210);
});
});
describe('sectionContainerStyles', () => {
it('returns style string for container element based on sectionShellWidth', () => {
expect(vm.sectionContainerStyles.width).toBe(`${mockShellWidth}px`);
});
});
});
});
...@@ -6,7 +6,7 @@ import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -6,7 +6,7 @@ import { getTimeframeForWeeksView } 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data'; import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
...@@ -15,8 +15,6 @@ const createComponent = ({ ...@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeWeeks, timeframe = mockTimeframeWeeks,
timeframeItem = mockTimeframeWeeks[0], timeframeItem = mockTimeframeWeeks[0],
epic = mockEpic, epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => { }) => {
const Component = Vue.extend(EpicItemTimelineComponent); const Component = Vue.extend(EpicItemTimelineComponent);
...@@ -25,8 +23,6 @@ const createComponent = ({ ...@@ -25,8 +23,6 @@ const createComponent = ({
timeframe, timeframe,
timeframeItem, timeframeItem,
epic, epic,
shellWidth,
itemWidth,
}); });
}; };
...@@ -70,22 +66,34 @@ describe('WeeksPresetMixin', () => { ...@@ -70,22 +66,34 @@ describe('WeeksPresetMixin', () => {
}); });
describe('isTimeframeUnderEndDateForWeek', () => { describe('isTimeframeUnderEndDateForWeek', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
beforeEach(() => { beforeEach(() => {
vm = createComponent({}); vm = createComponent({});
}); });
it('returns true if provided timeframeItem is under epicEndDate', () => { it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
const epicEndDate = new Date(2018, 0, 3); // Jan 3, 2018 const epicEndDate = new Date(2018, 0, 3); // Jan 3, 2018
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate)).toBe(true); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem)).toBe(true);
}); });
it('returns false if provided timeframeItem is NOT under epicEndDate', () => { it('returns false if provided timeframeItem is NOT under epicEndDate', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
const epicEndDate = new Date(2018, 0, 15); // Jan 15, 2018 const epicEndDate = new Date(2018, 0, 15); // Jan 15, 2018
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate)).toBe(false); vm = createComponent({
epic: Object.assign({}, mockEpic, {
endDate: epicEndDate,
}),
});
expect(vm.isTimeframeUnderEndDateForWeek(timeframeItem)).toBe(false);
}); });
}); });
...@@ -136,14 +144,13 @@ describe('WeeksPresetMixin', () => { ...@@ -136,14 +144,13 @@ describe('WeeksPresetMixin', () => {
}), }),
}); });
expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 51'); expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 38');
}); });
}); });
describe('getTimelineBarWidthForWeeks', () => { describe('getTimelineBarWidthForWeeks', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => { it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({ vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeWeeks[0], timeframeItem: mockTimeframeWeeks[0],
epic: Object.assign({}, mockEpic, { epic: Object.assign({}, mockEpic, {
startDate: new Date(2018, 0, 1), // Jan 1, 2018 startDate: new Date(2018, 0, 1), // Jan 1, 2018
...@@ -151,7 +158,7 @@ describe('WeeksPresetMixin', () => { ...@@ -151,7 +158,7 @@ describe('WeeksPresetMixin', () => {
}), }),
}); });
expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1611); expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1208);
}); });
}); });
}); });
......
...@@ -50,7 +50,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -50,7 +50,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('setInitialData', () => { describe('setInitialData', () => {
it('Should set initial roadmap props', done => { it('should set initial roadmap props', done => {
const mockRoadmap = { const mockRoadmap = {
foo: 'bar', foo: 'bar',
bar: 'baz', bar: 'baz',
...@@ -68,7 +68,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -68,7 +68,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('setWindowResizeInProgress', () => { describe('setWindowResizeInProgress', () => {
it('Should set value of `state.windowResizeInProgress` based on provided value', done => { it('should set value of `state.windowResizeInProgress` based on provided value', done => {
testAction( testAction(
actions.setWindowResizeInProgress, actions.setWindowResizeInProgress,
true, true,
...@@ -148,13 +148,13 @@ describe('Roadmap Vuex Actions', () => { ...@@ -148,13 +148,13 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('requestEpics', () => { describe('requestEpics', () => {
it('Should set `epicsFetchInProgress` to true', done => { it('should set `epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done); testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done);
}); });
}); });
describe('requestEpicsForTimeframe', () => { describe('requestEpicsForTimeframe', () => {
it('Should set `epicsFetchForTimeframeInProgress` to true', done => { it('should set `epicsFetchForTimeframeInProgress` to true', done => {
testAction( testAction(
actions.requestEpicsForTimeframe, actions.requestEpicsForTimeframe,
{}, {},
...@@ -167,7 +167,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -167,7 +167,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('receiveEpicsSuccess', () => { describe('receiveEpicsSuccess', () => {
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list', done => { it('should set formatted epics array and epicId to IDs array in state based on provided epics list', done => {
testAction( testAction(
actions.receiveEpicsSuccess, actions.receiveEpicsSuccess,
{ {
...@@ -200,7 +200,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -200,7 +200,7 @@ describe('Roadmap Vuex Actions', () => {
); );
}); });
it('Should set formatted epics array and epicId to IDs array in state based on provided epics list when timeframe was extended', 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( testAction(
actions.receiveEpicsSuccess, actions.receiveEpicsSuccess,
{ rawEpics: [mockRawEpic], newEpic: true, timeframeExtended: true }, { rawEpics: [mockRawEpic], newEpic: true, timeframeExtended: true },
...@@ -223,7 +223,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -223,7 +223,7 @@ describe('Roadmap Vuex Actions', () => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
}); });
it('Should set epicsFetchInProgress, epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', done => { it('should set epicsFetchInProgress, epicsFetchForTimeframeInProgress to false and epicsFetchFailure to true', done => {
testAction( testAction(
actions.receiveEpicsFailure, actions.receiveEpicsFailure,
{}, {},
...@@ -234,7 +234,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -234,7 +234,7 @@ describe('Roadmap Vuex Actions', () => {
); );
}); });
it('Should show flash error', () => { it('should show flash error', () => {
actions.receiveEpicsFailure({ commit: () => {} }); actions.receiveEpicsFailure({ commit: () => {} });
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
...@@ -255,7 +255,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -255,7 +255,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('success', () => { describe('success', () => {
it('Should dispatch requestEpics and receiveEpicsSuccess when request is successful', done => { it('should dispatch requestEpics and receiveEpicsSuccess when request is successful', done => {
mock.onGet(epicsPath).replyOnce(200, rawEpics); mock.onGet(epicsPath).replyOnce(200, rawEpics);
testAction( testAction(
...@@ -278,7 +278,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -278,7 +278,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('failure', () => { describe('failure', () => {
it('Should dispatch requestEpics and receiveEpicsFailure when request fails', done => { it('should dispatch requestEpics and receiveEpicsFailure when request fails', done => {
mock.onGet(epicsPath).replyOnce(500, {}); mock.onGet(epicsPath).replyOnce(500, {});
testAction( testAction(
...@@ -314,7 +314,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -314,7 +314,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('success', () => { describe('success', () => {
it('Should dispatch requestEpicsForTimeframe and receiveEpicsSuccess when request is successful', done => { it('should dispatch requestEpicsForTimeframe and receiveEpicsSuccess when request is successful', done => {
mock.onGet(mockEpicsPath).replyOnce(200, rawEpics); mock.onGet(mockEpicsPath).replyOnce(200, rawEpics);
testAction( testAction(
...@@ -337,7 +337,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -337,7 +337,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('failure', () => { describe('failure', () => {
it('Should dispatch requestEpicsForTimeframe and requestEpicsFailure when request fails', done => { it('should dispatch requestEpicsForTimeframe and requestEpicsFailure when request fails', done => {
mock.onGet(mockEpicsPath).replyOnce(500, {}); mock.onGet(mockEpicsPath).replyOnce(500, {});
testAction( testAction(
...@@ -360,7 +360,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -360,7 +360,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('extendTimeframe', () => { describe('extendTimeframe', () => {
it('Should prepend to timeframe when called with extend type prepend', done => { it('should prepend to timeframe when called with extend type prepend', done => {
testAction( testAction(
actions.extendTimeframe, actions.extendTimeframe,
{ extendAs: EXTEND_AS.PREPEND }, { extendAs: EXTEND_AS.PREPEND },
...@@ -371,7 +371,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -371,7 +371,7 @@ describe('Roadmap Vuex Actions', () => {
); );
}); });
it('Should append to timeframe when called with extend type append', done => { it('should append to timeframe when called with extend type append', done => {
testAction( testAction(
actions.extendTimeframe, actions.extendTimeframe,
{ extendAs: EXTEND_AS.APPEND }, { extendAs: EXTEND_AS.APPEND },
...@@ -384,7 +384,7 @@ describe('Roadmap Vuex Actions', () => { ...@@ -384,7 +384,7 @@ describe('Roadmap Vuex Actions', () => {
}); });
describe('refreshEpicDates', () => { describe('refreshEpicDates', () => {
it('Should update epics after refreshing epic dates to match with updated timeframe', done => { it('should update epics after refreshing epic dates to match with updated timeframe', done => {
const epics = rawEpics.map(epic => const epics = rawEpics.map(epic =>
epicUtils.formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate), epicUtils.formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
); );
...@@ -399,4 +399,17 @@ describe('Roadmap Vuex Actions', () => { ...@@ -399,4 +399,17 @@ describe('Roadmap Vuex Actions', () => {
); );
}); });
}); });
describe('setBufferSize', () => {
it('should set bufferSize in store state', done => {
testAction(
actions.setBufferSize,
10,
state,
[{ type: types.SET_BUFFER_SIZE, payload: 10 }],
[],
done,
);
});
});
}); });
...@@ -137,4 +137,14 @@ describe('Roadmap Store Mutations', () => { ...@@ -137,4 +137,14 @@ describe('Roadmap Store Mutations', () => {
expect(state.timeframe[1]).toBe(extendedTimeframe[0]); expect(state.timeframe[1]).toBe(extendedTimeframe[0]);
}); });
}); });
describe('SET_BUFFER_SIZE', () => {
it('Should set `bufferSize` in state', () => {
const bufferSize = 10;
mutations[types.SET_BUFFER_SIZE](state, bufferSize);
expect(state.bufferSize).toBe(bufferSize);
});
});
}); });
...@@ -11690,10 +11690,10 @@ vue-template-es2015-compiler@^1.9.0: ...@@ -11690,10 +11690,10 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-virtual-scroll-list@^1.3.1: vue-virtual-scroll-list@^1.4.4:
version "1.3.1" version "1.4.4"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76" resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.4.tgz#5fca7a13f785899bbfb70471ec4fe222437d8495"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g== integrity sha512-wU7FDpd9Xy4f62pf8SBg/ak21jMI/pdx4s4JPah+z/zuhmeAafQgp8BjtZvvt+b0BZOsOS1FJuCfUH7azTkivQ==
vue@^2.6.10: vue@^2.6.10:
version "2.6.10" version "2.6.10"
......
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