Commit f06a5b6e authored by Phil Hughes's avatar Phil Hughes

Merge branch '32822-roadmap-buffered-rendering' into 'master'

Use buffered rendering for Roadmap epics

See merge request gitlab-org/gitlab!19875
parents fe6c395e d57f62c4
...@@ -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: { computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
showTimelineBar() {
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.initMounted();
this.$nextTick(() => {
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', {
width: this.$el.clientWidth,
height: this.shellHeight,
});
},
/**
* 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 // We cannot scroll to the indicator immediately
if (approxChildrenHeight < this.shellHeight) { // on render as it will trigger scroll event leading
// reset approximate height and recalculate actual height // to timeline expand, so we wait for another render
approxChildrenHeight = 0; // cycle to complete.
children.forEach(child => { this.$nextTick(() => {
// accumulate children height this.scrollToTodayIndicator();
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
}); });
// set height and show empty row reducing horizontal scrollbar size if (!Object.keys(this.emptyRowContainerStyles).length) {
this.emptyRowHeight = this.shellHeight - approxChildrenHeight; this.emptyRowContainerStyles = this.getEmptyRowContainerStyles();
this.showEmptyRow = true; }
} else { });
this.showBottomShadow = true; },
getEmptyRowContainerStyles() {
if (this.$refs.epicItems && this.$refs.epicItems.length) {
return {
height: `${this.$el.clientHeight -
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">
<epic-item <template v-if="glFeatures.roadmapBufferedRendering && !emptyRowContainerVisible">
v-for="(epic, index) in epics" <virtual-list
:key="index" v-if="epics.length"
:preset-type="presetType" :size="$options.epicItemHeight"
:epic="epic" :remain="bufferSize"
:timeframe="timeframe" :bench="bufferSize"
:current-group-id="currentGroupId" :scrollelement="roadmapShellEl"
:shell-width="sectionShellWidth" :item="$options.EpicItem"
:item-width="sectionItemWidth" :itemcount="epics.length"
/> :itemprops="getEpicItemProps"
/>
</template>
<template v-else>
<epic-item
v-for="(epic, index) in epics"
ref="epicItems"
:key="index"
:preset-type="presetType"
:epic="epic"
:timeframe="timeframe"
:current-group-id="currentGroupId"
/>
</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,71 +35,25 @@ export default { ...@@ -35,71 +35,25 @@ 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 // We're guarding this as in tests, `roadmapTimeline`
// provide accurate size of viewport until child contents are // is not ready when this line is executed.
// actually loaded and rendered into the DOM, hence if (this.$refs.roadmapTimeline) {
// we wait for nextTick which ensures DOM update has completed this.timeframeStartOffset = this.$refs.roadmapTimeline.$el
// before setting shellWidth .querySelector('.timeline-header-item')
// see https://vuejs.org/v2/api/#Vue-nextTick .querySelector('.item-sublabel .sublabel-value:first-child')
if (this.$el.parentElement) { .getBoundingClientRect().left;
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`
// is not ready when this line is executed.
if (this.$refs.roadmapTimeline) {
this.timeframeStartOffset = this.$refs.roadmapTimeline.$el
.querySelector('.timeline-header-item')
.querySelector('.item-sublabel .sublabel-value:first-child')
.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();
......
...@@ -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'); const stylesObj = vm.getTodayBarStyles();
expect(stylesObj.left).toBe('50%');
expect(vm.todayBarReady).toBe(true);
});
it('sets `todayBarReady` prop based on value of provided `todayBarReady` param', () => { expect(stylesObj.height).toBe('calc(100vh - 16px)');
vm = createComponent({}); expect(stylesObj.left).toBe('50%');
vm.handleEpicsListRender({
todayBarReady: false,
});
expect(vm.todayBarReady).toBe(false);
}); });
}); });
}); });
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);
});
});
}); });
...@@ -11710,10 +11710,10 @@ vue-template-es2015-compiler@^1.9.0: ...@@ -11710,10 +11710,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