Commit 64c40b6c authored by Kushal Pandya's avatar Kushal Pandya

Update to support presetType

parent 806f78fc
<script>
import { totalDaysInMonth } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import tooltip from '~/vue_shared/directives/tooltip';
import {
EPIC_DETAILS_CELL_WIDTH,
TIMELINE_CELL_MIN_WIDTH,
TIMELINE_END_OFFSET_FULL,
TIMELINE_END_OFFSET_HALF,
} from '../constants';
import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
export default {
directives: {
tooltip,
import {
EPIC_DETAILS_CELL_WIDTH,
TIMELINE_CELL_MIN_WIDTH,
TIMELINE_END_OFFSET_FULL,
TIMELINE_END_OFFSET_HALF,
PRESET_TYPES,
} from '../constants';
export default {
directives: {
tooltip,
},
mixins: [
QuartersPresetMixin,
MonthsPresetMixin,
WeeksPresetMixin,
],
props: {
presetType: {
type: String,
required: true,
},
timeframe: {
type: Array,
required: true,
},
timeframeItem: {
type: [Date, Object],
required: true,
},
epic: {
type: Object,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
props: {
timeframe: {
type: Array,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
epic: {
type: Object,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
data() {
},
data() {
return {
timelineBarReady: false,
timelineBarStyles: '',
};
},
computed: {
itemStyles() {
return {
timelineBarReady: false,
timelineBarStyles: '',
width: `${this.itemWidth}px`,
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
showTimelineBar() {
return this.hasStartDate();
},
showTimelineBar() {
return this.hasStartDate();
},
watch: {
shellWidth: function shellWidth() {
// Render timeline bar only when shellWidth is updated.
this.renderTimelineBar();
},
},
watch: {
shellWidth: function shellWidth() {
// Render timeline bar only when shellWidth is updated.
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);
},
/**
* Check if current timeline cell has start date for current epic
*/
hasStartDate() {
return this.epic.startDate.getMonth() === this.timeframeItem.getMonth() &&
this.epic.startDate.getFullYear() === this.timeframeItem.getFullYear();
},
/**
* In case startDate for any epic is undefined or is out of range
* for current timeframe, we have to provide specific offset while
* positioning it to ensure that;
*
* 1. Timeline bar starts at correct position based on start date.
* 2. Bar starts exactly at the start of cell in case start date is `1`.
* 3. A "triangle" shape is shown at the beginning of timeline bar
* when startDate is out of range.
*/
getTimelineBarStartOffset() {
const daysInMonth = totalDaysInMonth(this.timeframeItem);
const startDate = this.epic.startDate.getDate();
let offset = '';
if (this.epic.startDateOutOfRange ||
(this.epic.startDateUndefined && this.epic.endDateOutOfRange)) {
// If Epic startDate is out of timeframe range
// OR
// Epic startDate is undefined AND Epic endDate is out of timeframe range
// no offset is needed.
offset = '';
} else if (startDate === 1) {
// If Epic startDate is first day of the month
// Set offset to 0.
offset = 'left: 0;';
} else {
// If Epic end date is out of range
const lastTimeframeItem = this.timeframe[this.timeframe.length - 1];
// Check if Epic start date falls within last month of the timeframe
if (this.epic.startDate.getMonth() === lastTimeframeItem.getMonth() &&
this.epic.startDate.getFullYear() === lastTimeframeItem.getFullYear()) {
// Compensate for triangle size
offset = `right: ${TIMELINE_END_OFFSET_HALF}px;`;
} else {
// Calculate proportional offset based on startDate and total days in
// current month.
offset = `left: ${(startDate / daysInMonth) * 100}%;`;
}
}
return offset;
},
/**
* In case startDate or endDate for any epic is undefined or is out of range
* for current timeframe, we have to provide specific offset while
* setting width to ensure that;
*
* 1. Timeline bar ends at correct position based on end date.
* 2. A "triangle" shape is shown at the end of timeline bar
* when endDate is out of range.
*/
getTimelineBarEndOffset() {
let offset = 0;
if ((this.epic.startDateOutOfRange && this.epic.endDateOutOfRange) ||
(this.epic.startDateUndefined && this.epic.endDateOutOfRange)) {
// If Epic startDate is undefined or out of range
// AND
// endDate is out of range
// Reduce offset size from the width to compensate for fadeout of timelinebar
// and/or showing triangle at the end and beginning
offset = TIMELINE_END_OFFSET_FULL;
} else if (this.epic.endDateOutOfRange) {
// If Epic end date is out of range
// Reduce offset size from the width to compensate for triangle (which is sized at 8px)
offset = TIMELINE_END_OFFSET_HALF;
} else {
// No offset needed if all dates are defined.
offset = 0;
}
return offset;
},
/**
* Check if current timeframe is under the range of Epic endDate
*/
isTimeframeUnderEndDate(timeframeItem, epicEndDate) {
return timeframeItem.getYear() <= epicEndDate.getYear() &&
timeframeItem.getMonth() === epicEndDate.getMonth();
},
/**
* Get width for timeline bar for current cell (representing a month)
* Based on total days in the month and width of month on UI
*/
getBarWidthForMonth(cellWidth, daysInMonth, date) {
const dayWidth = cellWidth / daysInMonth;
const barWidth = date === daysInMonth ? cellWidth : dayWidth * date;
return Math.min(cellWidth, barWidth);
},
/**
* This method is externally only called when current timeframe cell has timeline
* bar to show. So when this method is called, we iterate over entire timeframe
* array starting from current timeframeItem.
*
* For eg;
* If timeframe range for 6 months is;
* 2017 Oct, 2017 Nov, 2017 Dec, 2018 Jan, 2018 Feb, 2018 Mar
*
* And if Epic starts in 2017 Dec and ends in 2018 Feb.
*
* Then this method will iterate over timeframe as;
* 2017 Dec => 2018 Feb
* And will add up width(see 1.) for timeline bar for each month in iteration
* based on provided start and end dates.
*
* 1. Width from date is calculated by totalWidthCell / totalDaysInMonth = widthOfSingleDay
* and then dateOfMonth x widthOfSingleDay = totalBarWidth
*/
getTimelineBarWidth() {
let timelineBarWidth = 0;
const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const offsetEnd = this.getTimelineBarEndOffset();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
// Start iteration from current month
for (let i = indexOfCurrentMonth; i < this.timeframe.length; i += 1) {
// Get total days for current month
const daysInMonth = totalDaysInMonth(this.timeframe[i]);
},
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;
if (i === indexOfCurrentMonth) {
// If this is current month
if (this.isTimeframeUnderEndDate(this.timeframe[i], epicEndDate)) {
// 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.getBarWidthForMonth(
cellWidth,
daysInMonth,
((epicEndDate.getDate() - epicStartDate.getDate()) + 1),
);
// Break as Epic start and end date fall within current timeframe month itself!
break;
} else {
// Epic end date does NOT fall in current month.
return Math.max(minWidth, TIMELINE_CELL_MIN_WIDTH);
},
hasStartDate() {
if (this.presetType === PRESET_TYPES.QUARTERS) {
return this.hasStartDateForQuarter();
} else if (this.presetType === PRESET_TYPES.MONTHS) {
return this.hasStartDateForMonth();
} else if (this.presetType === PRESET_TYPES.WEEKS) {
return this.hasStartDateForWeek();
}
return false;
},
getTimelineBarEndOffsetHalf() {
if (this.presetType === PRESET_TYPES.QUARTERS) {
return TIMELINE_END_OFFSET_HALF;
} else if (this.presetType === PRESET_TYPES.MONTHS) {
return TIMELINE_END_OFFSET_HALF;
} else if (this.presetType === PRESET_TYPES.WEEKS) {
return this.getTimelineBarEndOffsetHalfForWeek();
}
return 0;
},
/**
* In case startDate or endDate for any epic is undefined or is out of range
* for current timeframe, we have to provide specific offset while
* setting width to ensure that;
*
* 1. Timeline bar ends at correct position based on end date.
* 2. A "triangle" shape is shown at the end of timeline bar
* when endDate is out of range.
*/
getTimelineBarEndOffset() {
let offset = 0;
// 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();
timelineBarWidth += this.getBarWidthForMonth(cellWidth, daysInMonth, date);
}
} else if (this.isTimeframeUnderEndDate(this.timeframe[i], epicEndDate)) {
// If this is NOT current month but epicEndDate falls under
// current timeframe month then calculate width
// based on date of the month
timelineBarWidth += this.getBarWidthForMonth(
cellWidth,
daysInMonth,
epicEndDate.getDate(),
);
// Break as Epic end date falls within current timeframe month!
break;
} else {
// This is neither current month,
// nor does the Epic end date fall under current timeframe month
// add width for entire cell of current timeframe.
timelineBarWidth += this.getBarWidthForMonth(cellWidth, daysInMonth, daysInMonth);
}
}
if (
(this.epic.startDateOutOfRange && this.epic.endDateOutOfRange) ||
(this.epic.startDateUndefined && this.epic.endDateOutOfRange)
) {
// If Epic startDate is undefined or out of range
// AND
// endDate is out of range
// Reduce offset size from the width to compensate for fadeout of timelinebar
// and/or showing triangle at the end and beginning
offset = TIMELINE_END_OFFSET_FULL;
} else if (this.epic.endDateOutOfRange) {
// If Epic end date is out of range
// Reduce offset size from the width to compensate for triangle (which is sized at 8px)
offset = this.getTimelineBarEndOffsetHalf();
} else {
// No offset needed if all dates are defined.
offset = 0;
}
// Reduce any offset from total width and round it off.
return timelineBarWidth - offsetEnd;
},
/**
* Renders timeline bar only if current
* timeframe item has startDate for the epic.
*/
renderTimelineBar() {
if (this.hasStartDate()) {
this.timelineBarStyles = `width: ${this.getTimelineBarWidth()}px; ${this.getTimelineBarStartOffset()}`;
this.timelineBarReady = true;
return offset;
},
/**
* Renders timeline bar only if current
* timeframe item has startDate for the epic.
*/
renderTimelineBar() {
if (this.hasStartDate()) {
if (this.presetType === PRESET_TYPES.QUARTERS) {
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
} else if (this.presetType === PRESET_TYPES.MONTHS) {
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`;
} else if (this.presetType === PRESET_TYPES.WEEKS) {
this.timelineBarStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`;
}
},
this.timelineBarReady = true;
}
},
};
},
};
</script>
<template>
......
<script>
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import NewEpic from '../../epics/new_epic/components/new_epic.vue';
import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
export default {
components: {
NewEpic,
import NewEpic from '../../epics/new_epic/components/new_epic.vue';
export default {
components: {
NewEpic,
},
props: {
presetType: {
type: String,
required: true,
},
timeframeStart: {
type: [Date, Object],
required: true,
},
timeframeEnd: {
type: [Date, Object],
required: true,
},
hasFiltersApplied: {
type: Boolean,
required: true,
},
props: {
timeframeStart: {
type: Date,
required: true,
},
timeframeEnd: {
type: Date,
required: true,
},
hasFiltersApplied: {
type: Boolean,
required: true,
},
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: {
type: String,
required: true,
},
newEpicEndpoint: {
type: String,
required: true,
},
computed: {
timeframeRange() {
const startDate = dateInWords(
emptyStateIllustrationPath: {
type: String,
required: true,
},
},
computed: {
timeframeRange() {
let startDate;
let endDate;
if (this.presetType === PRESET_TYPES.QUARTERS) {
const quarterStart = this.timeframeStart.range[0];
const quarterEnd = this.timeframeEnd.range[2];
startDate = dateInWords(
quarterStart,
true,
quarterStart.getFullYear() === quarterEnd.getFullYear(),
);
endDate = dateInWords(quarterEnd, true);
} else if (this.presetType === PRESET_TYPES.MONTHS) {
startDate = dateInWords(
this.timeframeStart,
true,
this.timeframeStart.getFullYear() === this.timeframeEnd.getFullYear(),
);
const endDate = dateInWords(this.timeframeEnd, true);
endDate = dateInWords(this.timeframeEnd, true);
} else if (this.presetType === PRESET_TYPES.WEEKS) {
const end = new Date(this.timeframeEnd.getTime());
end.setDate(end.getDate() + 6);
return {
startDate,
endDate,
};
},
message() {
if (this.hasFiltersApplied) {
return s__('GroupRoadmap|Sorry, no epics matched your search');
}
return s__('GroupRoadmap|The roadmap shows the progress of your epics along a timeline');
},
subMessage() {
if (this.hasFiltersApplied) {
return sprintf(s__('GroupRoadmap|To widen your search, change or remove filters. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}.'), {
startDate: this.timeframeRange.startDate,
endDate: this.timeframeRange.endDate,
});
}
return sprintf(s__('GroupRoadmap|To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from %{startDate} to %{endDate}.'), {
startDate = dateInWords(
this.timeframeStart,
true,
this.timeframeStart.getFullYear() === end.getFullYear(),
);
endDate = dateInWords(end, true);
}
return {
startDate,
endDate,
};
},
message() {
if (this.hasFiltersApplied) {
return s__('GroupRoadmap|Sorry, no epics matched your search');
}
return s__('GroupRoadmap|The roadmap shows the progress of your epics along a timeline');
},
subMessage() {
if (this.hasFiltersApplied) {
return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateWithFilters, {
startDate: this.timeframeRange.startDate,
endDate: this.timeframeRange.endDate,
});
},
}
return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateDefault, {
startDate: this.timeframeRange.startDate,
endDate: this.timeframeRange.endDate,
});
},
};
},
};
</script>
<template>
......
<script>
import eventHub from '../event_hub';
import { PRESET_TYPES } from '../constants';
import SectionMixin from '../mixins/section_mixin';
import timelineHeaderItem from './timeline_header_item.vue';
import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue';
import MonthsHeaderItem from './preset_months/months_header_item.vue';
import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue';
export default {
components: {
timelineHeaderItem,
QuartersHeaderItem,
MonthsHeaderItem,
WeeksHeaderItem,
},
mixins: [
SectionMixin,
],
props: {
presetType: {
type: String,
required: true,
},
epics: {
type: Array,
required: true,
......@@ -35,6 +45,18 @@
scrolledHeaderClass: '',
};
},
computed: {
headerItemComponentForPreset() {
if (this.presetType === PRESET_TYPES.QUARTERS) {
return 'quarters-header-item';
} else if (this.presetType === PRESET_TYPES.MONTHS) {
return 'months-header-item';
} else if (this.presetType === PRESET_TYPES.WEEKS) {
return 'weeks-header-item';
}
return '';
},
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
},
......@@ -57,7 +79,8 @@
:style="sectionContainerStyles"
>
<span class="timeline-header-blank"></span>
<timeline-header-item
<component
:is="headerItemComponentForPreset"
v-for="(timeframeItem, index) in timeframe"
:key="index"
:timeframe-index="index"
......
<script>
import { totalDaysInMonth } from '~/lib/utils/datetime_utility';
import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES } from '../constants';
export default {
props: {
currentDate: {
type: Date,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
import eventHub from '../event_hub';
export default {
props: {
presetType: {
type: String,
required: true,
},
data() {
return {
todayBarStyles: '',
todayBarReady: false,
};
currentDate: {
type: Date,
required: true,
},
mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
timeframeItem: {
type: [Date, Object],
required: true,
},
beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
},
data() {
return {
todayBarStyles: '',
todayBarReady: false,
};
},
mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
},
beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
},
methods: {
/**
* This method takes height of current shell
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ height }) {
let left = 0;
// Get total days of current timeframe Item and then
// get size in % from current date and days in range
// based on the current presetType
if (this.presetType === PRESET_TYPES.QUARTERS) {
left = Math.floor(
dayInQuarter(this.currentDate, this.timeframeItem.range) /
totalDaysInQuarter(this.timeframeItem.range) *
100,
);
} else if (this.presetType === PRESET_TYPES.MONTHS) {
left = Math.floor(this.currentDate.getDate() / totalDaysInMonth(this.timeframeItem) * 100);
} else if (this.presetType === PRESET_TYPES.WEEKS) {
left = Math.floor(((this.currentDate.getDay() + 1) / 7 * 100) - 7);
}
// We add 20 to height to ensure that
// today indicator goes past the bottom
// edge of the browser even when
// scrollbar is present
this.todayBarStyles = {
height: `${height + 20}px`,
left: `${left}%`,
};
this.todayBarReady = true;
},
methods: {
/**
* This method takes height of current shell
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ height }) {
// Get total days of current timeframe Item
const daysInMonth = totalDaysInMonth(this.timeframeItem);
// Get size in % from current date and days in month.
const left = Math.floor((this.currentDate.getDate() / daysInMonth) * 100);
handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x;
const rootOffsetLeft = this.$root.$el.offsetLeft;
// We add 20 to height to ensure that
// today indicator goes past the bottom
// edge of the browser even when
// scrollbar is present
this.todayBarStyles = {
height: `${height + 20}px`,
left: `${left}%`,
};
this.todayBarReady = true;
},
// 3px to compensate size of bubble on top of Indicator
this.todayBarReady = (indicatorX - rootOffsetLeft) >= (EPIC_DETAILS_CELL_WIDTH + 3);
},
};
},
};
</script>
<template>
<span
v-if="todayBarReady"
class="today-bar"
:class="{ 'invisible': !todayBarReady }"
:style="todayBarStyles"
>
</span>
......
import Vue from 'vue';
import epicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import { TIMELINE_CELL_MIN_WIDTH, TIMELINE_END_OFFSET_FULL, TIMELINE_END_OFFSET_HALF } from 'ee/roadmap/constants';
import {
TIMELINE_END_OFFSET_FULL,
TIMELINE_END_OFFSET_HALF,
TIMELINE_CELL_MIN_WIDTH,
PRESET_TYPES,
} from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframe, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
import { mockTimeframeMonths, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
const createComponent = ({
timeframe = mockTimeframe,
timeframeItem = mockTimeframe[0],
presetType = PRESET_TYPES.MONTHS,
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
shellWidth = mockShellWidth,
itemWidth = mockItemWidth,
......@@ -16,6 +22,7 @@ const createComponent = ({
const Component = Vue.extend(epicItemTimelineComponent);
return mountComponent(Component, {
presetType,
timeframe,
timeframeItem,
epic,
......@@ -52,7 +59,7 @@ describe('EpicItemTimelineComponent', () => {
describe('getCellWidth', () => {
it('returns proportionate width based on timeframe length and shellWidth', () => {
vm = createComponent({});
expect(vm.getCellWidth()).toBe(280);
expect(vm.getCellWidth()).toBe(240);
});
it('returns minimum fixed width when proportionate width available lower than minimum fixed width defined', () => {
......@@ -63,71 +70,6 @@ describe('EpicItemTimelineComponent', () => {
});
});
describe('hasStartDate', () => {
it('returns true when Epic.startDate falls within timeframeItem', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframe[1] }),
timeframeItem: mockTimeframe[1],
});
expect(vm.showTimelineBar).toBe(true);
});
it('returns false when Epic.startDate does not fall within timeframeItem', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframe[0] }),
timeframeItem: mockTimeframe[1],
});
expect(vm.showTimelineBar).toBe(false);
});
});
describe('getTimelineBarStartOffset', () => {
it('returns empty string when Epic startDate is out of range', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDateOutOfRange: true }),
});
expect(vm.getTimelineBarStartOffset()).toBe('');
});
it('returns empty string when Epic startDate is undefined and endDate is out of range', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDateUndefined: true,
endDateOutOfRange: true,
}),
});
expect(vm.getTimelineBarStartOffset()).toBe('');
});
it('return `left: 0;` when Epic startDate is first day of the month', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: new Date(2018, 0, 1),
}),
});
expect(vm.getTimelineBarStartOffset()).toBe('left: 0;');
});
it('returns `right: 8px;` when Epic startDate is in last timeframe month and endDate is out of range', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframe[mockTimeframe.length - 1],
endDateOutOfRange: true,
}),
});
expect(vm.getTimelineBarStartOffset()).toBe('right: 8px;');
});
it('returns proportional `left` value based on Epic startDate and days in the month', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: new Date(2018, 0, 15),
}),
});
expect(vm.getTimelineBarStartOffset()).toBe('left: 50%;');
});
});
describe('getTimelineBarEndOffset', () => {
it('returns full offset value when both Epic startDate and endDate is out of range', () => {
vm = createComponent({
......@@ -164,62 +106,21 @@ describe('EpicItemTimelineComponent', () => {
});
});
describe('isTimeframeUnderEndDate', () => {
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.isTimeframeUnderEndDate(timeframeItem, epicEndDate)).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.isTimeframeUnderEndDate(timeframeItem, epicEndDate)).toBe(false);
});
});
describe('getBarWidthForMonth', () => {
it('returns calculated bar width based on provided cellWidth, daysInMonth and date', () => {
vm = createComponent({});
expect(vm.getBarWidthForMonth(300, 30, 1)).toBe(10); // 10% size
expect(vm.getBarWidthForMonth(300, 30, 15)).toBe(150); // 50% size
expect(vm.getBarWidthForMonth(300, 30, 30)).toBe(300); // Full size
});
});
describe('getTimelineBarWidth', () => {
it('returns calculated width value based on Epic.startDate and Epic.endDate', () => {
vm = createComponent({
shellWidth: 2000,
timeframeItem: mockTimeframe[0],
epic: Object.assign({}, mockEpic, {
startDate: new Date(2017, 11, 15), // Dec 15, 2017
endDate: new Date(2018, 1, 15), // Feb 15, 2017
}),
});
expect(vm.getTimelineBarWidth()).toBe(850);
});
});
describe('renderTimelineBar', () => {
it('sets `timelineBarStyles` & `timelineBarReady` when timeframeItem has Epic.startDate', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframe[1] }),
timeframeItem: mockTimeframe[1],
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('width: 1400px; left: 0;');
expect(vm.timelineBarStyles).toBe('width: 1216px; 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: mockTimeframe[0] }),
timeframeItem: mockTimeframe[1],
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[0] }),
timeframeItem: mockTimeframeMonths[1],
});
vm.renderTimelineBar();
expect(vm.timelineBarStyles).toBe('');
......@@ -241,16 +142,16 @@ describe('EpicItemTimelineComponent', () => {
it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => {
vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframe[1] }),
timeframeItem: mockTimeframe[1],
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
timeframeItem: mockTimeframeMonths[1],
});
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', done => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframe[0],
startDate: mockTimeframeMonths[0],
endDate: new Date(2018, 1, 15),
}),
});
......@@ -258,16 +159,16 @@ describe('EpicItemTimelineComponent', () => {
vm.renderTimelineBar();
vm.$nextTick(() => {
expect(timelineBarEl.getAttribute('style')).toBe('width: 990px; left: 0px;');
expect(timelineBarEl.getAttribute('style')).toBe('width: 608.571px; 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 => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDateUndefined: true,
startDate: mockTimeframe[0],
startDate: mockTimeframeMonths[0],
}),
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
......@@ -279,11 +180,11 @@ describe('EpicItemTimelineComponent', () => {
});
});
it('renders timeline bar with `start-date-outside` class when Epic startDate is out of range of timeframe', (done) => {
it('renders timeline bar with `start-date-outside` class when Epic startDate is out of range of timeframe', done => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDateOutOfRange: true,
startDate: mockTimeframe[0],
startDate: mockTimeframeMonths[0],
originalStartDate: new Date(2017, 0, 1),
}),
});
......@@ -296,12 +197,12 @@ describe('EpicItemTimelineComponent', () => {
});
});
it('renders timeline bar with `end-date-undefined` class when Epic endDate is undefined', (done) => {
it('renders timeline bar with `end-date-undefined` class when Epic endDate is undefined', done => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframe[0],
startDate: mockTimeframeMonths[0],
endDateUndefined: true,
endDate: mockTimeframe[mockTimeframe.length - 1],
endDate: mockTimeframeMonths[mockTimeframeMonths.length - 1],
}),
});
const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
......@@ -313,12 +214,12 @@ describe('EpicItemTimelineComponent', () => {
});
});
it('renders timeline bar with `end-date-outside` class when Epic endDate is out of range of timeframe', (done) => {
it('renders timeline bar with `end-date-outside` class when Epic endDate is out of range of timeframe', done => {
vm = createComponent({
epic: Object.assign({}, mockEpic, {
startDate: mockTimeframe[0],
startDate: mockTimeframeMonths[0],
endDateOutOfRange: true,
endDate: mockTimeframe[mockTimeframe.length - 1],
endDate: mockTimeframeMonths[mockTimeframeMonths.length - 1],
originalEndDate: new Date(2018, 11, 1),
}),
});
......
......@@ -2,15 +2,29 @@ import Vue from 'vue';
import epicsListEmptyComponent from 'ee/roadmap/components/epics_list_empty.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframe, mockSvgPath, mockNewEpicEndpoint } from '../mock_data';
import { PRESET_TYPES } from 'ee/roadmap/constants';
const createComponent = (hasFiltersApplied = false) => {
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockTimeframeQuarters,
mockTimeframeMonths,
mockTimeframeWeeks,
mockSvgPath,
mockNewEpicEndpoint,
} from '../mock_data';
const createComponent = ({
hasFiltersApplied = false,
presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0],
timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1],
}) => {
const Component = Vue.extend(epicsListEmptyComponent);
return mountComponent(Component, {
timeframeStart: mockTimeframe[0],
timeframeEnd: mockTimeframe[mockTimeframe.length - 1],
presetType,
timeframeStart,
timeframeEnd,
emptyStateIllustrationPath: mockSvgPath,
newEpicEndpoint: mockNewEpicEndpoint,
hasFiltersApplied,
......@@ -21,7 +35,7 @@ describe('EpicsListEmptyComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
vm = createComponent({});
});
afterEach(() => {
......@@ -46,25 +60,93 @@ describe('EpicsListEmptyComponent', () => {
});
describe('subMessage', () => {
it('returns default empty state sub-message', () => {
expect(vm.subMessage).toBe('To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. Only epics in the past 3 months and the next 3 months are shown &ndash; from Nov 1, 2017 to Apr 30, 2018.');
describe('with presetType `QUARTERS`', () => {
beforeEach(() => {
vm.presetType = PRESET_TYPES.QUARTERS;
vm.timeframeStart = mockTimeframeQuarters[0];
vm.timeframeEnd = mockTimeframeQuarters[mockTimeframeQuarters.length - 1];
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', done => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from Oct 1, 2017 to Mar 31, 2019.');
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', done => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To widen your search, change or remove filters. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from Oct 1, 2017 to Mar 31, 2019.');
})
.then(done)
.catch(done.fail);
});
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', done => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To widen your search, change or remove filters. Only epics in the past 3 months and the next 3 months are shown &ndash; from Nov 1, 2017 to Apr 30, 2018.');
})
.then(done)
.catch(done.fail);
describe('with presetType `MONTHS`', () => {
beforeEach(() => {
vm.presetType = PRESET_TYPES.MONTHS;
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', done => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from Dec 1, 2017 to Jun 30, 2018.');
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', done => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To widen your search, change or remove filters. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from Dec 1, 2017 to Jun 30, 2018.');
})
.then(done)
.catch(done.fail);
});
});
describe('with presetType `WEEKS`', () => {
beforeEach(() => {
const timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1];
timeframeEnd.setDate(timeframeEnd.getDate() + 6);
vm.presetType = PRESET_TYPES.WEEKS;
vm.timeframeStart = mockTimeframeWeeks[0];
vm.timeframeEnd = timeframeEnd;
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', done => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To view the roadmap, add a planned start or finish date to one of your epics in this group or its subgroups. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from Dec 24, 2017 to Feb 9, 2018.');
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', done => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe('To widen your search, change or remove filters. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from Dec 24, 2017 to Feb 15, 2018.');
})
.then(done)
.catch(done.fail);
});
});
});
describe('timeframeRange', () => {
it('returns correct timeframe startDate and endDate in words', () => {
expect(vm.timeframeRange.startDate).toBe('Nov 1, 2017');
expect(vm.timeframeRange.endDate).toBe('Apr 30, 2018');
expect(vm.timeframeRange.startDate).toBe('Dec 1, 2017');
expect(vm.timeframeRange.endDate).toBe('Jun 30, 2018');
});
});
});
......
......@@ -3,18 +3,22 @@ import Vue from 'vue';
import roadmapTimelineSectionComponent from 'ee/roadmap/components/roadmap_timeline_section.vue';
import eventHub from 'ee/roadmap/event_hub';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockEpic, mockTimeframe, mockShellWidth } from '../mock_data';
import { mockEpic, mockTimeframeMonths, mockShellWidth } from '../mock_data';
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
epics = [mockEpic],
timeframe = mockTimeframe,
timeframe = mockTimeframeMonths,
shellWidth = mockShellWidth,
listScrollable = false,
}) => {
const Component = Vue.extend(roadmapTimelineSectionComponent);
return mountComponent(Component, {
presetType,
epics,
timeframe,
shellWidth,
......
......@@ -2,23 +2,26 @@ import Vue from 'vue';
import timelineTodayIndicatorComponent from 'ee/roadmap/components/timeline_today_indicator.vue';
import eventHub from 'ee/roadmap/event_hub';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframe } from '../mock_data';
import { mockTimeframeMonths } from '../mock_data';
const mockCurrentDate = new Date(
mockTimeframe[0].getFullYear(),
mockTimeframe[0].getMonth(),
mockTimeframeMonths[0].getFullYear(),
mockTimeframeMonths[0].getMonth(),
15,
);
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
currentDate = mockCurrentDate,
timeframeItem = mockTimeframe[0],
timeframeItem = mockTimeframeMonths[0],
}) => {
const Component = Vue.extend(timelineTodayIndicatorComponent);
return mountComponent(Component, {
presetType,
currentDate,
timeframeItem,
});
......@@ -48,7 +51,7 @@ describe('TimelineTodayIndicatorComponent', () => {
});
const stylesObj = vm.todayBarStyles;
expect(stylesObj.height).toBe('120px');
expect(stylesObj.left).toBe('50%');
expect(stylesObj.left).toBe('48%');
expect(vm.todayBarReady).toBe(true);
});
});
......
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