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) => {
* @param {Date} date
* @param {Array} quarter
*/
export const dayInQuarter = (date, quarter) =>
quarter.reduce((acc, month) => {
if (date.getMonth() > month.getMonth()) {
export const dayInQuarter = (date, quarter) => {
const dateValues = {
date: date.getDate(),
month: date.getMonth(),
};
return quarter.reduce((acc, month) => {
if (dateValues.month > month.getMonth()) {
return acc + totalDaysInMonth(month);
} else if (date.getMonth() === month.getMonth()) {
return acc + date.getDate();
} else if (dateValues.month === month.getMonth()) {
return acc + dateValues.date;
}
return acc + 0;
}, 0);
};
window.gl = window.gl || {};
window.gl.utils = {
......
- page_classes = page_class << @html_class
- page_classes = page_classes.flatten.compact
!!! 5
%html{ lang: I18n.locale, class: page_class }
%html{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
......
......@@ -28,14 +28,6 @@ export default {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
updated() {
this.removeHighlight();
......@@ -75,8 +67,6 @@ export default {
:timeframe="timeframe"
:timeframe-item="timeframeItem"
:epic="epic"
:shell-width="shellWidth"
:item-width="itemWidth"
/>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
epic: {
type: Object,
......@@ -78,22 +74,13 @@ export default {
<template>
<span class="epic-details-cell" data-qa-selector="epic_details_cell">
<div class="epic-title">
<a v-tooltip :href="epic.webUrl" :title="epic.title" data-container="body" class="epic-url">
{{ epic.title }}
</a>
<a :href="epic.webUrl" :title="epic.title" class="epic-url">{{ epic.title }}</a>
</div>
<div class="epic-group-timeframe">
<span
v-if="isEpicGroupDifferent"
v-tooltip
:title="epic.groupFullName"
class="epic-group"
data-placement="right"
data-container="body"
<span v-if="isEpicGroupDifferent" :title="epic.groupFullName" class="epic-group"
>{{ epic.groupName }} &middot;</span
>
{{ epic.groupName }} &middot;
</span>
<span class="epic-timeframe" v-html="timeframeString"> </span>
<span class="epic-timeframe" v-html="timeframeString"></span>
</div>
</span>
</template>
......@@ -5,11 +5,10 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
export default {
cellWidth: TIMELINE_CELL_MIN_WIDTH,
directives: {
tooltip,
},
......@@ -31,56 +30,28 @@ export default {
type: Object,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const { startDate, endDate } = this.epic;
return {
timelineBarReady: false,
timelineBarStyles: '',
epicStartDateValues: {
day: startDate.getDay(),
date: startDate.getDate(),
month: startDate.getMonth(),
year: startDate.getFullYear(),
time: startDate.getTime(),
},
epicEndDateValues: {
day: endDate.getDay(),
date: endDate.getDate(),
month: endDate.getMonth(),
year: endDate.getFullYear(),
time: endDate.getTime(),
},
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
showTimelineBar() {
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() {
if (this.presetType === PRESET_TYPES.QUARTERS) {
return this.hasStartDateForQuarter();
......@@ -91,35 +62,33 @@ export default {
}
return false;
},
/**
* Renders timeline bar only if current
* timeframe item has startDate for the epic.
*/
renderTimelineBar() {
if (this.hasStartDate()) {
timelineBarStyles() {
let barStyles = {};
if (this.hasStartDate) {
if (this.presetType === PRESET_TYPES.QUARTERS) {
// 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
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
barStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
} else if (this.presetType === PRESET_TYPES.MONTHS) {
// 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) {
// 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>
<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">
<a
v-if="showTimelineBar"
v-if="hasStartDate"
:href="epic.webUrl"
:class="{
'start-date-undefined': epic.startDateUndefined,
......@@ -127,8 +96,7 @@ export default {
}"
:style="timelineBarStyles"
class="timeline-bar"
>
</a>
></a>
</div>
</span>
</template>
<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 {
EpicItem,
epicItemHeight: EPIC_ITEM_HEIGHT,
components: {
epicItem,
VirtualList,
EpicItem,
},
mixins: [SectionMixin],
mixins: [glFeatureFlagsMixin()],
props: {
presetType: {
type: String,
......@@ -29,33 +35,23 @@ export default {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
},
data() {
return {
shellHeight: 0,
emptyRowHeight: 0,
showEmptyRow: false,
offsetLeft: 0,
emptyRowContainerStyles: {},
showBottomShadow: false,
roadmapShellEl: null,
};
},
computed: {
emptyRowContainerStyles() {
return {
height: `${this.emptyRowHeight}px`,
};
...mapState(['bufferSize']),
emptyRowContainerVisible() {
return this.epics.length < this.bufferSize;
},
emptyRowCellStyles() {
sectionContainerStyles() {
return {
width: `${this.sectionItemWidth}px`,
width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
};
},
shadowCellStyles() {
......@@ -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() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleTimelineRefresh);
this.$nextTick(() => {
this.initMounted();
});
this.initMounted();
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleTimelineRefresh);
},
methods: {
...mapActions(['setBufferSize']),
initMounted() {
// Get available shell height based on viewport height
this.shellHeight = window.innerHeight - this.$el.offsetTop;
this.roadmapShellEl = this.$root.$el && this.$root.$el.firstChild;
this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
// In case there are epics present, initialize empty row
if (this.epics.length) {
this.initEmptyRow();
}
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;
// Wait for component render to complete
this.$nextTick(() => {
this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0;
// Check if approximate height is greater than shell height
if (approxChildrenHeight < this.shellHeight) {
// reset approximate height and recalculate actual height
approxChildrenHeight = 0;
children.forEach(child => {
// accumulate children height
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
// We cannot scroll to the indicator immediately
// on render as it will trigger scroll event leading
// to timeline expand, so we wait for another render
// cycle to complete.
this.$nextTick(() => {
this.scrollToTodayIndicator();
});
// set height and show empty row reducing horizontal scrollbar size
this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
this.showEmptyRow = true;
} else {
this.showBottomShadow = true;
if (!Object.keys(this.emptyRowContainerStyles).length) {
this.emptyRowContainerStyles = this.getEmptyRowContainerStyles();
}
});
},
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
* by half the column size
*/
scrollToTodayIndicator() {
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;
if (this.$el.parentElement) this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
handleEpicsListScroll({ 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>
<template>
<div :style="sectionContainerStyles" class="epics-list-section">
<epic-item
v-for="(epic, index) in epics"
:key="index"
:preset-type="presetType"
:epic="epic"
:timeframe="timeframe"
:current-group-id="currentGroupId"
:shell-width="sectionShellWidth"
:item-width="sectionItemWidth"
/>
<template v-if="glFeatures.roadmapBufferedRendering && !emptyRowContainerVisible">
<virtual-list
v-if="epics.length"
:size="$options.epicItemHeight"
:remain="bufferSize"
:bench="bufferSize"
:scrollelement="roadmapShellEl"
:item="$options.EpicItem"
:itemcount="epics.length"
:itemprops="getEpicItemProps"
/>
</template>
<template v-else>
<epic-item
v-for="(epic, index) in epics"
ref="epicItems"
:key="index"
:preset-type="presetType"
:epic="epic"
:timeframe="timeframe"
:current-group-id="currentGroupId"
/>
</template>
<div
v-if="showEmptyRow"
v-if="emptyRowContainerVisible"
:style="emptyRowContainerStyles"
class="epics-list-item epics-list-item-empty clearfix"
>
......@@ -188,11 +158,9 @@ export default {
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
:style="emptyRowCellStyles"
class="epic-timeline-cell"
>
</span>
></span>
</div>
<div v-if="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
<div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
</div>
</template>
......@@ -20,10 +20,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -36,11 +32,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
timelineHeaderLabel() {
const year = this.timeframeItem.getFullYear();
const month = monthInWords(this.timeframeItem, true);
......@@ -85,7 +76,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<months-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
......@@ -18,10 +18,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -32,11 +28,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
quarterBeginDate() {
return this.timeframeItem.range[0];
},
......@@ -66,7 +57,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<quarters-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
......@@ -20,10 +20,6 @@ export default {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
......@@ -34,11 +30,6 @@ export default {
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
......@@ -77,7 +68,7 @@ export default {
</script>
<template>
<span :style="itemStyles" class="timeline-header-item">
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
......
<script>
import { mapState, mapActions } from 'vuex';
import _ from 'underscore';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
......@@ -34,7 +33,6 @@ export default {
data() {
const roadmapGraphQL = gon.features && gon.features.roadmapGraphql;
return {
handleResizeThrottled: {},
// TODO
// Remove these method alias and call actual
// method once feature flag is removed.
......@@ -73,25 +71,8 @@ export default {
);
},
},
watch: {
epicsFetchInProgress(value) {
if (!value && this.epics.length) {
this.$nextTick(() => {
eventHub.$emit('refreshTimeline', {
todayBarReady: true,
initialRender: true,
});
});
}
},
},
mounted() {
this.fetchEpicsFn();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
},
methods: {
...mapActions([
......@@ -103,28 +84,6 @@ export default {
'extendTimeframe',
'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)
* We need performing following actions;
......
......@@ -3,7 +3,7 @@ import { mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
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 epicsListSection from './epics_list_section.vue';
......@@ -35,71 +35,25 @@ export default {
},
data() {
return {
shellWidth: 0,
shellHeight: 0,
noScroll: false,
timeframeStartOffset: 0,
};
},
computed: {
...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() {
eventHub.$on('refreshTimeline', this.handleEpicsListRendered);
this.$nextTick(() => {
// Client width at the time of component mount will not
// provide accurate size of viewport until child contents are
// actually loaded and rendered into the DOM, hence
// we wait for nextTick which ensures DOM update has completed
// before setting shellWidth
// see https://vuejs.org/v2/api/#Vue-nextTick
if (this.$el.parentElement) {
this.shellHeight = (this.defaultInnerHeight || window.innerHeight) - this.$el.offsetTop;
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
this.shellWidth = this.getShellWidth(this.noScroll);
// We're guarding this as in tests, `roadmapTimeline`
// 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;
}
// 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: {
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() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
......@@ -124,20 +78,12 @@ export default {
</script>
<template>
<div
:class="{ 'prevent-vertical-scroll': noScroll }"
:style="containerStyles"
class="roadmap-shell"
data-qa-selector="roadmap_shell"
@scroll="handleScroll"
>
<div class="roadmap-shell" data-qa-selector="roadmap_shell" @scroll="handleScroll">
<roadmap-timeline-section
ref="roadmapTimeline"
:preset-type="presetType"
:epics="epics"
:timeframe="timeframe"
:shell-width="shellWidth"
:list-scrollable="!noScroll"
/>
<div v-if="!epics.length" class="skeleton-loader js-skeleton-loader">
<div v-for="n in 10" :key="n" class="mt-2">
......@@ -145,12 +91,11 @@ export default {
</div>
</div>
<epics-list-section
v-else
:preset-type="presetType"
:epics="epics"
:timeframe="timeframe"
:shell-width="shellWidth"
:current-group-id="currentGroupId"
:list-scrollable="!noScroll"
/>
</div>
</template>
<script>
import eventHub from '../event_hub';
import { PRESET_TYPES } from '../constants';
import SectionMixin from '../mixins/section_mixin';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue';
import MonthsHeaderItem from './preset_months/months_header_item.vue';
......@@ -15,7 +13,6 @@ export default {
MonthsHeaderItem,
WeeksHeaderItem,
},
mixins: [SectionMixin],
props: {
presetType: {
type: String,
......@@ -29,14 +26,6 @@ export default {
type: Array,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
},
data() {
return {
......@@ -54,6 +43,11 @@ export default {
}
return '';
},
sectionContainerStyles() {
return {
width: `${EPIC_DETAILS_CELL_WIDTH + TIMELINE_CELL_MIN_WIDTH * this.timeframe.length}px`,
};
},
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
......@@ -84,7 +78,6 @@ export default {
:timeframe-index="index"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:item-width="sectionItemWidth"
/>
</div>
</template>
<script>
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';
......@@ -22,29 +22,22 @@ export default {
},
data() {
return {
todayBarStyles: '',
todayBarReady: false,
todayBarStyles: {},
todayBarReady: true,
};
},
mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleEpicsListRender);
this.$nextTick(() => {
this.todayBarStyles = this.getTodayBarStyles();
});
},
beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleEpicsListRender);
},
methods: {
/**
* This method takes height of current shell
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ todayBarReady }) {
let left = 0;
let height = 0;
getTodayBarStyles() {
let left;
// Get total days of current timeframe Item and then
// get size in % from current date and days in range
......@@ -63,21 +56,10 @@ export default {
left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK);
}
// On initial load, container element height is 0
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`,
return {
left: `${left}%`,
height: `calc(100vh - ${this.$el.getBoundingClientRect().y + SCROLL_BAR_SIZE}px)`,
};
this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
},
handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x;
......@@ -91,5 +73,5 @@ export default {
</script>
<template>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"> </span>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"></span>
</template>
......@@ -6,12 +6,14 @@ export const EPIC_ITEM_HEIGHT = 50;
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 DAYS_IN_WEEK = 7;
export const BUFFER_OVERLAP_SIZE = 20;
export const PRESET_TYPES = {
QUARTERS: 'QUARTERS',
MONTHS: 'MONTHS',
......
......@@ -7,18 +7,18 @@ export default {
*/
hasStartDateForMonth() {
return (
this.epic.startDate.getMonth() === this.timeframeItem.getMonth() &&
this.epic.startDate.getFullYear() === this.timeframeItem.getFullYear()
this.epicStartDateValues.month === this.timeframeItem.getMonth() &&
this.epicStartDateValues.year === this.timeframeItem.getFullYear()
);
},
/**
* Check if current epic ends within current month (timeline cell)
*/
isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate) {
if (epicEndDate.getFullYear() <= timeframeItem.getFullYear()) {
return epicEndDate.getMonth() === timeframeItem.getMonth();
isTimeframeUnderEndDateForMonth(timeframeItem) {
if (this.epicEndDateValues.year <= timeframeItem.getFullYear()) {
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
......@@ -42,7 +42,7 @@ export default {
*/
getTimelineBarStartOffsetForMonths() {
const daysInMonth = totalDaysInMonth(this.timeframeItem);
const startDate = this.epic.startDate.getDate();
const startDate = this.epicStartDateValues.date;
if (
this.epic.startDateOutOfRange ||
......@@ -88,9 +88,9 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
const { cellWidth } = this.$options;
const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epicEndDateValues;
// Start iteration from current month
for (let i = indexOfCurrentMonth; i < this.timeframe.length; i += 1) {
......@@ -99,13 +99,13 @@ export default {
if (i === indexOfCurrentMonth) {
// 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
// then get width for number of days between start and end dates (inclusive)
timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth,
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;
......@@ -115,18 +115,17 @@ export default {
// If start date is first day of the 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.
const date =
epicStartDate.getDate() === 1 ? daysInMonth : daysInMonth - epicStartDate.getDate();
const date = epicStartDate.date === 1 ? daysInMonth : daysInMonth - epicStartDate.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
// current timeframe month then calculate width
// based on date of the month
timelineBarWidth += this.getBarWidthForSingleMonth(
cellWidth,
daysInMonth,
epicEndDate.getDate(),
epicEndDate.date,
);
// Break as Epic end date falls within current timeframe month!
break;
......
......@@ -10,17 +10,17 @@ export default {
const quarterEnd = this.timeframeItem.range[2];
return (
this.epic.startDate.getTime() >= quarterStart.getTime() &&
this.epic.startDate.getTime() <= quarterEnd.getTime()
this.epicStartDateValues.time >= quarterStart.getTime() &&
this.epicStartDateValues.time <= quarterEnd.getTime()
);
},
/**
* Check if current epic ends within current quarter (timeline cell)
*/
isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate) {
isTimeframeUnderEndDateForQuarter(timeframeItem) {
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
......@@ -89,7 +89,7 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const { cellWidth } = this.$options;
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
......@@ -97,7 +97,7 @@ export default {
const currentQuarter = this.timeframe[i].range;
if (i === indexOfCurrentQuarter) {
if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) {
if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth,
totalDaysInQuarter(currentQuarter),
......@@ -117,7 +117,7 @@ export default {
date,
);
}
} else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i], epicEndDate)) {
} else if (this.isTimeframeUnderEndDateForQuarter(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleQuarter(
cellWidth,
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 {
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
return (
this.epic.startDate.getTime() >= firstDayOfWeek.getTime() &&
this.epic.startDate.getTime() <= lastDayOfWeek.getTime()
this.epicStartDateValues.time >= firstDayOfWeek.getTime() &&
this.epicStartDateValues.time <= lastDayOfWeek.getTime()
);
},
/**
......@@ -26,9 +26,9 @@ export default {
/**
* Check if current epic ends within current week (timeline cell)
*/
isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate) {
isTimeframeUnderEndDateForWeek(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
......@@ -55,8 +55,8 @@ export default {
*/
getTimelineBarStartOffsetForWeeks() {
const daysInWeek = 7;
const dayWidth = this.getCellWidth() / daysInWeek;
const startDate = this.epic.startDate.getDay() + 1;
const dayWidth = this.$options.cellWidth / daysInWeek;
const startDate = this.epicStartDateValues.day + 1;
const firstDayOfWeek = this.timeframeItem.getDay() + 1;
if (
......@@ -98,24 +98,24 @@ export default {
let timelineBarWidth = 0;
const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
const { cellWidth } = this.$options;
const epicStartDate = this.epicStartDateValues;
const epicEndDate = this.epicEndDateValues;
for (let i = indexOfCurrentWeek; i < this.timeframe.length; i += 1) {
if (i === indexOfCurrentWeek) {
if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) {
if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek(
cellWidth,
epicEndDate.getDay() - epicStartDate.getDay() + 1,
epicEndDate.day - epicStartDate.day + 1,
);
break;
} else {
const date = epicStartDate.getDay() === 0 ? 7 : 7 - epicStartDate.getDay();
const date = epicStartDate.day === 0 ? 7 : 7 - epicStartDate.day;
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, date);
}
} else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i], epicEndDate)) {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.getDay() + 1);
} else if (this.isTimeframeUnderEndDateForWeek(this.timeframe[i])) {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, epicEndDate.day + 1);
break;
} else {
timelineBarWidth += this.getBarWidthForSingleWeek(cellWidth, 7);
......
......@@ -84,7 +84,7 @@ export const receiveEpicsSuccess = (
// Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline
if (
formattedEpic.startDate <= formattedEpic.endDate &&
formattedEpic.startDate.getTime() <= formattedEpic.endDate.getTime() &&
state.epicIds.indexOf(formattedEpic.id) < 0
) {
Object.assign(formattedEpic, {
......@@ -192,5 +192,7 @@ export const refreshEpicDates = ({ commit, state, getters }) => {
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
export default () => {};
......@@ -15,3 +15,5 @@ export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const PREPEND_TIMEFRAME = 'PREPEND_TIMEFRAME';
export const APPEND_TIMEFRAME = 'APPEND_TIMEFRAME';
export const SET_BUFFER_SIZE = 'SET_BUFFER_SIZE';
......@@ -50,4 +50,8 @@ export default {
state.extendedTimeframe = extendedTimeframe;
state.timeframe.push(...extendedTimeframe);
},
[types.SET_BUFFER_SIZE](state, bufferSize) {
state.bufferSize = bufferSize;
},
};
......@@ -9,6 +9,7 @@ export default () => ({
// Data
epicIid: '',
epics: [],
visibleEpics: [],
epicIds: [],
currentGroupId: -1,
fullPath: '',
......@@ -16,6 +17,7 @@ export default () => ({
extendedTimeframe: [],
presetType: '',
sortedBy: '',
bufferSize: 0,
// UI Flags
defaultInnerHeight: 0,
......
......@@ -19,30 +19,43 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
}
@keyframes fadeInDetails {
from {
opacity: 0;
}
@mixin roadmap-scroll-mixin {
height: $grid-size;
width: $details-cell-width;
pointer-events: none;
}
to {
opacity: 1;
html.group-epics-roadmap-html {
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 {
from {
opacity: 0;
.group-epics-roadmap-body {
height: calc(100% - #{$header-height});
.page-with-contextual-sidebar {
height: 100%;
}
to {
opacity: 0.75;
.group-epics-roadmap {
// 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 {
height: $grid-size;
width: $details-cell-width;
pointer-events: none;
.group-epics-roadmap-wrapper,
.group-epics-roadmap .content {
height: 100%;
}
}
.epics-details-filters {
......@@ -100,6 +113,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-container {
overflow: hidden;
height: 100%;
&.overflow-reset {
overflow: initial;
......@@ -108,12 +122,13 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-shell {
position: relative;
height: 100%;
width: 100%;
overflow-x: auto;
.skeleton-loader {
position: absolute;
top: $header-item-height;
left: $timeline-cell-width / 2;
width: $details-cell-width;
height: 100%;
padding-top: $gl-padding-top;
......@@ -171,6 +186,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.timeline-header-item {
width: $timeline-cell-width;
&:last-of-type .item-label {
border-right: 0;
}
......@@ -241,6 +258,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.epics-list-section {
height: calc(100% - 60px);
.epics-list-item {
&:hover {
.epic-details-cell,
......@@ -250,6 +269,8 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
&.epics-list-item-empty {
height: 100%;
&:hover {
.epic-details-cell,
.epic-timeline-cell {
......@@ -295,7 +316,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.epic-title,
.epic-group-timeframe {
animation: fadeInDetails 1s;
will-change: contents;
}
.epic-title {
......@@ -327,6 +348,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.epic-timeline-cell {
width: $timeline-cell-width;
background-color: transparent;
border-right: $border-style;
......@@ -341,7 +363,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
background-color: $blue-500;
border-radius: $border-radius-default;
opacity: 0.75;
animation: fadeinTimelineBar 1s;
will-change: width, left;
&:hover {
opacity: 1;
......
......@@ -12,6 +12,7 @@ module Groups
before_action :persist_roadmap_layout, only: [:show]
before_action do
push_frontend_feature_flag(:roadmap_graphql, @group)
push_frontend_feature_flag(:roadmap_buffered_rendering, @group)
end
# show roadmap for a group
......
- @no_breadcrumb_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_class = "group-epics-roadmap"
- breadcrumb_title _("Epics Roadmap")
......
......@@ -76,18 +76,6 @@ describe 'group epic roadmap', :js do
expect(page).to have_selector('.epics-list-item .epic-title', count: 3)
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
describe 'roadmap page with epics state filter' do
......
......@@ -140,7 +140,7 @@ describe('EpicItemDetailsComponent', () => {
expect(epicGroupNameEl).not.toBeNull();
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', () => {
......
......@@ -9,13 +9,7 @@ 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 {
mockTimeframeInitialDate,
mockEpic,
mockGroupId,
mockShellWidth,
mockItemWidth,
} from '../mock_data';
import { mockTimeframeInitialDate, mockEpic, mockGroupId } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -24,8 +18,6 @@ const createComponent = ({
epic = mockEpic,
timeframe = mockTimeframeMonths,
currentGroupId = mockGroupId,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(epicItemComponent);
......@@ -34,8 +26,6 @@ const createComponent = ({
epic,
timeframe,
currentGroupId,
shellWidth,
itemWidth,
});
};
......
import Vue from '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 { 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -16,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(epicItemTimelineComponent);
......@@ -26,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -42,80 +37,25 @@ describe('EpicItemTimelineComponent', () => {
it('returns default data props', () => {
vm = createComponent({});
expect(vm.timelineBarReady).toBe(false);
expect(vm.timelineBarStyles).toBe('');
});
});
describe('computed', () => {
describe('itemStyles', () => {
it('returns CSS min-width based on getCellWidth() method', () => {
vm = createComponent({});
expect(vm.itemStyles.width).toBe(`${mockItemWidth}px`);
});
});
});
describe('methods', () => {
describe('getCellWidth', () => {
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));
expect(vm.epicStartDateValues).toEqual(
jasmine.objectContaining({
day: mockEpic.startDate.getDay(),
date: mockEpic.startDate.getDate(),
month: mockEpic.startDate.getMonth(),
year: mockEpic.startDate.getFullYear(),
time: mockEpic.startDate.getTime(),
}),
);
expect(vm.epicEndDateValues).toEqual(
jasmine.objectContaining({
day: mockEpic.endDate.getDay(),
date: mockEpic.endDate.getDate(),
month: mockEpic.endDate.getMonth(),
year: mockEpic.endDate.getFullYear(),
time: mockEpic.endDate.getTime(),
}),
);
});
});
......@@ -126,12 +66,6 @@ describe('EpicItemTimelineComponent', () => {
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', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
......@@ -141,7 +75,7 @@ describe('EpicItemTimelineComponent', () => {
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({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeMonths[0],
......@@ -150,11 +84,8 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.getAttribute('style')).toBe('width: 742.5px; left: 0px;');
done();
});
expect(timelineBarEl.getAttribute('style')).toContain('width');
expect(timelineBarEl.getAttribute('style')).toContain('left: 0px;');
});
it('renders timeline bar with `start-date-undefined` class when Epic startDate is undefined', done => {
......@@ -166,7 +97,6 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('start-date-undefined')).toBe(true);
done();
......@@ -183,7 +113,6 @@ describe('EpicItemTimelineComponent', () => {
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.classList.contains('end-date-undefined')).toBe(true);
done();
......
......@@ -4,7 +4,7 @@ import MonthsHeaderItemComponent from 'ee/roadmap/components/preset_months/month
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
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 mockTimeframeIndex = 0;
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeMonths[mockTimeframeIndex],
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(MonthsHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -46,14 +42,6 @@ describe('MonthsHeaderItemComponent', () => {
});
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', () => {
it('returns string containing Year and Month for current timeline header item', () => {
vm = createComponent({});
......
......@@ -4,7 +4,7 @@ import QuartersHeaderItemComponent from 'ee/roadmap/components/preset_quarters/q
import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
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 mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeQuarters[mockTimeframeIndex],
timeframe = mockTimeframeQuarters,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(QuartersHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -44,14 +40,6 @@ describe('QuartersHeaderItemComponent', () => {
});
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', () => {
it('returns date object representing quarter begin date for current `timeframeItem`', () => {
expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]);
......
......@@ -4,7 +4,7 @@ import WeeksHeaderItemComponent from 'ee/roadmap/components/preset_weeks/weeks_h
import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
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 mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
......@@ -13,8 +13,6 @@ const createComponent = ({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeWeeks[mockTimeframeIndex],
timeframe = mockTimeframeWeeks,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(WeeksHeaderItemComponent);
......@@ -22,8 +20,6 @@ const createComponent = ({
timeframeIndex,
timeframeItem,
timeframe,
shellWidth,
itemWidth,
});
};
......@@ -44,14 +40,6 @@ describe('WeeksHeaderItemComponent', () => {
});
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', () => {
it('returns date object representing last day of the week as set in `timeframeItem`', () => {
expect(vm.lastDayOfCurrentWeek.getDate()).toBe(
......
......@@ -59,10 +59,6 @@ describe('Roadmap AppComponent', () => {
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.handleResizeThrottled).toBeDefined();
});
describe('when `gon.feature.roadmapGraphql` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
......@@ -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', () => {
it('renders roadmap container with class `roadmap-container`', () => {
expect(vm.$el.classList.contains('roadmap-container')).toBe(true);
......
......@@ -43,8 +43,9 @@ const createComponent = (
describe('RoadmapShellComponent', () => {
let vm;
beforeEach(() => {
beforeEach(done => {
vm = createComponent({});
vm.$nextTick(done);
});
afterEach(() => {
......@@ -53,39 +54,10 @@ describe('RoadmapShellComponent', () => {
describe('data', () => {
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);
});
});
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', () => {
beforeEach(() => {
document.body.innerHTML +=
......@@ -103,7 +75,6 @@ describe('RoadmapShellComponent', () => {
Vue.nextTick()
.then(() => {
vmWithParentEl.noScroll = false;
vmWithParentEl.handleScroll();
expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object));
......@@ -131,13 +102,5 @@ describe('RoadmapShellComponent', () => {
.then(done)
.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';
import { PRESET_TYPES } from 'ee/roadmap/constants';
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);
......@@ -15,17 +15,13 @@ 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,
});
};
......@@ -33,7 +29,7 @@ describe('RoadmapTimelineSectionComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
vm = createComponent();
});
afterEach(() => {
......@@ -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('handleEpicsListScroll', () => {
it('sets `scrolled-ahead` class on thead element based on provided scrollTop value', () => {
......
......@@ -42,62 +42,47 @@ describe('TimelineTodayIndicatorComponent', () => {
it('returns default data props', () => {
vm = createComponent({});
expect(vm.todayBarStyles).toBe('');
expect(vm.todayBarReady).toBe(false);
expect(vm.todayBarStyles).toEqual({});
expect(vm.todayBarReady).toBe(true);
});
});
describe('methods', () => {
describe('handleEpicsListRender', () => {
describe('getTodayBarStyles', () => {
it('sets `todayBarStyles` and `todayBarReady` props', () => {
vm = createComponent({});
vm.handleEpicsListRender({});
const stylesObj = vm.todayBarStyles;
expect(stylesObj.height).toBe('600px');
expect(stylesObj.left).toBe('50%');
expect(vm.todayBarReady).toBe(true);
});
const stylesObj = vm.getTodayBarStyles();
it('sets `todayBarReady` prop based on value of provided `todayBarReady` param', () => {
vm = createComponent({});
vm.handleEpicsListRender({
todayBarReady: false,
});
expect(vm.todayBarReady).toBe(false);
expect(stylesObj.height).toBe('calc(100vh - 16px)');
expect(stylesObj.left).toBe('50%');
});
});
});
describe('mounted', () => {
it('binds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
it('binds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$on');
const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
it('unbinds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$off');
const vmX = createComponent({});
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
});
});
describe('template', () => {
it('renders component container element with class `today-bar`', done => {
vm = createComponent({});
vm.handleEpicsListRender({});
vm.$nextTick(() => {
expect(vm.$el.classList.contains('today-bar')).toBe(true);
done();
......
......@@ -6,7 +6,7 @@ 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 { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeInitialDate, mockEpic } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -59,22 +55,34 @@ describe('MonthsPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForMonth', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 2018
beforeEach(() => {
vm = createComponent({});
});
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
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', () => {
const timeframeItem = new Date(2018, 0, 10); // Jan 10, 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', () => {
describe('getTimelineBarWidthForMonths', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeMonths[0],
epic: Object.assign({}, mockEpic, {
startDate: new Date(2017, 11, 15), // Dec 15, 2017
......@@ -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';
import { PRESET_TYPES } from 'ee/roadmap/constants';
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);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeQuarters,
timeframeItem = mockTimeframeQuarters[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -59,22 +55,34 @@ describe('QuartersPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForQuarter', () => {
const timeframeItem = mockTimeframeQuarters[1];
beforeEach(() => {
vm = createComponent({});
});
it('returns true if provided timeframeItem is under epicEndDate', () => {
const timeframeItem = mockTimeframeQuarters[1];
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', () => {
const timeframeItem = mockTimeframeQuarters[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', () => {
describe('getTimelineBarWidthForQuarters', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeQuarters[0],
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframeQuarters[0].range[1],
......@@ -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';
import { PRESET_TYPES } from 'ee/roadmap/constants';
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);
......@@ -15,8 +15,6 @@ const createComponent = ({
timeframe = mockTimeframeWeeks,
timeframeItem = mockTimeframeWeeks[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
}) => {
const Component = Vue.extend(EpicItemTimelineComponent);
......@@ -25,8 +23,6 @@ const createComponent = ({
timeframe,
timeframeItem,
epic,
shellWidth,
itemWidth,
});
};
......@@ -70,22 +66,34 @@ describe('WeeksPresetMixin', () => {
});
describe('isTimeframeUnderEndDateForWeek', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 2018
beforeEach(() => {
vm = createComponent({});
});
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
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', () => {
const timeframeItem = new Date(2018, 0, 7); // Jan 7, 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', () => {
}),
});
expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 51');
expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 38');
});
});
describe('getTimelineBarWidthForWeeks', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframeWeeks[0],
epic: Object.assign({}, mockEpic, {
startDate: new Date(2018, 0, 1), // Jan 1, 2018
......@@ -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', () => {
});
describe('setInitialData', () => {
it('Should set initial roadmap props', done => {
it('should set initial roadmap props', done => {
const mockRoadmap = {
foo: 'bar',
bar: 'baz',
......@@ -68,7 +68,7 @@ describe('Roadmap Vuex Actions', () => {
});
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(
actions.setWindowResizeInProgress,
true,
......@@ -148,13 +148,13 @@ describe('Roadmap Vuex Actions', () => {
});
describe('requestEpics', () => {
it('Should set `epicsFetchInProgress` to true', done => {
it('should set `epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done);
});
});
describe('requestEpicsForTimeframe', () => {
it('Should set `epicsFetchForTimeframeInProgress` to true', done => {
it('should set `epicsFetchForTimeframeInProgress` to true', done => {
testAction(
actions.requestEpicsForTimeframe,
{},
......@@ -167,7 +167,7 @@ describe('Roadmap Vuex Actions', () => {
});
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(
actions.receiveEpicsSuccess,
{
......@@ -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(
actions.receiveEpicsSuccess,
{ rawEpics: [mockRawEpic], newEpic: true, timeframeExtended: true },
......@@ -223,7 +223,7 @@ describe('Roadmap Vuex Actions', () => {
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(
actions.receiveEpicsFailure,
{},
......@@ -234,7 +234,7 @@ describe('Roadmap Vuex Actions', () => {
);
});
it('Should show flash error', () => {
it('should show flash error', () => {
actions.receiveEpicsFailure({ commit: () => {} });
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
......@@ -255,7 +255,7 @@ describe('Roadmap Vuex Actions', () => {
});
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);
testAction(
......@@ -278,7 +278,7 @@ describe('Roadmap Vuex Actions', () => {
});
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, {});
testAction(
......@@ -314,7 +314,7 @@ describe('Roadmap Vuex Actions', () => {
});
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);
testAction(
......@@ -337,7 +337,7 @@ describe('Roadmap Vuex Actions', () => {
});
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, {});
testAction(
......@@ -360,7 +360,7 @@ describe('Roadmap Vuex Actions', () => {
});
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(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.PREPEND },
......@@ -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(
actions.extendTimeframe,
{ extendAs: EXTEND_AS.APPEND },
......@@ -384,7 +384,7 @@ describe('Roadmap Vuex Actions', () => {
});
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 =>
epicUtils.formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
);
......@@ -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', () => {
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:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-virtual-scroll-list@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g==
vue-virtual-scroll-list@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.4.tgz#5fca7a13f785899bbfb70471ec4fe222437d8495"
integrity sha512-wU7FDpd9Xy4f62pf8SBg/ak21jMI/pdx4s4JPah+z/zuhmeAafQgp8BjtZvvt+b0BZOsOS1FJuCfUH7azTkivQ==
vue@^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