Commit a0ee4a9b authored by Kushal Pandya's avatar Kushal Pandya

Roadmap UX enhancements

Adds skeleton loader for Epics on default load.
Makes re-rendering Epics more efficient.
parent ead50a4a
...@@ -3,7 +3,6 @@ import _ from 'underscore'; ...@@ -3,7 +3,6 @@ import _ from 'underscore';
import Flash from '~/flash'; import Flash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { GlLoadingIcon } from '@gitlab/ui';
import epicsListEmpty from './epics_list_empty.vue'; import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue'; import roadmapShell from './roadmap_shell.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -14,7 +13,6 @@ export default { ...@@ -14,7 +13,6 @@ export default {
components: { components: {
epicsListEmpty, epicsListEmpty,
roadmapShell, roadmapShell,
GlLoadingIcon,
}, },
props: { props: {
store: { store: {
...@@ -44,7 +42,7 @@ export default { ...@@ -44,7 +42,7 @@ export default {
}, },
data() { data() {
return { return {
isLoading: true, isLoading: false,
isEpicsListEmpty: false, isEpicsListEmpty: false,
hasError: false, hasError: false,
handleResizeThrottled: {}, handleResizeThrottled: {},
...@@ -86,9 +84,16 @@ export default { ...@@ -86,9 +84,16 @@ export default {
.getEpics() .getEpics()
.then(res => res.data) .then(res => res.data)
.then(epics => { .then(epics => {
this.isLoading = false;
if (epics.length) { if (epics.length) {
this.store.setEpics(epics); this.store.setEpics(epics);
this.$nextTick(() => {
// Render timeline bars as we're already having timeline
// rendered before fetch
eventHub.$emit('refreshTimeline', {
todayBarReady: true,
initialRender: true,
});
});
} else { } else {
this.isEpicsListEmpty = true; this.isEpicsListEmpty = true;
} }
...@@ -107,14 +112,15 @@ export default { ...@@ -107,14 +112,15 @@ export default {
.then(epics => { .then(epics => {
if (epics.length) { if (epics.length) {
this.store.addEpics(epics); this.store.addEpics(epics);
this.$nextTick(() => {
// Re-render timeline bars with updated timeline
eventHub.$emit('refreshTimeline', {
height: window.innerHeight - roadmapTimelineEl.offsetTop,
todayBarReady: extendType === EXTEND_AS.PREPEND,
});
});
} }
this.$nextTick(() => {
// Re-render timeline bars with updated timeline
this.processExtendedTimeline({
itemsCount: timeframe ? timeframe.length : 0,
extendType,
roadmapTimelineEl,
});
});
}) })
.catch(() => { .catch(() => {
this.hasError = true; this.hasError = true;
...@@ -156,7 +162,6 @@ export default { ...@@ -156,7 +162,6 @@ export default {
processExtendedTimeline({ extendType = EXTEND_AS.PREPEND, roadmapTimelineEl, itemsCount = 0 }) { processExtendedTimeline({ extendType = EXTEND_AS.PREPEND, roadmapTimelineEl, itemsCount = 0 }) {
// Re-render timeline bars with updated timeline // Re-render timeline bars with updated timeline
eventHub.$emit('refreshTimeline', { eventHub.$emit('refreshTimeline', {
height: window.innerHeight - roadmapTimelineEl.offsetTop,
todayBarReady: extendType === EXTEND_AS.PREPEND, todayBarReady: extendType === EXTEND_AS.PREPEND,
}); });
...@@ -172,16 +177,10 @@ export default { ...@@ -172,16 +177,10 @@ export default {
handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) { handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) {
const timeframe = this.store.extendTimeframe(extendType); const timeframe = this.store.extendTimeframe(extendType);
this.$nextTick(() => { this.$nextTick(() => {
this.processExtendedTimeline({
itemsCount: timeframe ? timeframe.length : 0,
extendType,
roadmapTimelineEl,
});
this.fetchEpicsForTimeframe({ this.fetchEpicsForTimeframe({
timeframe, timeframe,
roadmapTimelineEl,
extendType, extendType,
roadmapTimelineEl,
}); });
}); });
}, },
...@@ -191,12 +190,6 @@ export default { ...@@ -191,12 +190,6 @@ export default {
<template> <template>
<div :class="{ 'overflow-reset': isEpicsListEmpty }" class="roadmap-container"> <div :class="{ 'overflow-reset': isEpicsListEmpty }" class="roadmap-container">
<gl-loading-icon
v-if="isLoading"
:label="s__('GroupRoadmap|Loading roadmap')"
:size="2"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<roadmap-shell <roadmap-shell
v-if="showRoadmap" v-if="showRoadmap"
:preset-type="presetType" :preset-type="presetType"
......
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
}, },
}, },
watch: { watch: {
shellWidth: function shellWidth() { shellWidth() {
// Render timeline bar only when shellWidth is updated. // Render timeline bar only when shellWidth is updated.
this.renderTimelineBar(); this.renderTimelineBar();
}, },
......
...@@ -3,7 +3,7 @@ import eventHub from '../event_hub'; ...@@ -3,7 +3,7 @@ import eventHub from '../event_hub';
import SectionMixin from '../mixins/section_mixin'; import SectionMixin from '../mixins/section_mixin';
import { TIMELINE_CELL_MIN_WIDTH } from '../constants'; import { TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
import epicItem from './epic_item.vue'; import epicItem from './epic_item.vue';
...@@ -74,18 +74,14 @@ export default { ...@@ -74,18 +74,14 @@ export default {
}, },
mounted() { mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll); eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', () => { eventHub.$on('refreshTimeline', this.handleTimelineRefresh);
this.initEmptyRow(false);
});
this.$nextTick(() => { this.$nextTick(() => {
this.initMounted(); this.initMounted();
}); });
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll); eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', () => { eventHub.$off('refreshTimeline', this.handleTimelineRefresh);
this.initEmptyRow(false);
});
}, },
methods: { methods: {
initMounted() { initMounted() {
...@@ -112,7 +108,7 @@ export default { ...@@ -112,7 +108,7 @@ export default {
* based on height of available list items and sets it to component * based on height of available list items and sets it to component
* props. * props.
*/ */
initEmptyRow(showEmptyRow = false) { initEmptyRow() {
const children = this.$children; const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length; let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
...@@ -130,7 +126,6 @@ export default { ...@@ -130,7 +126,6 @@ export default {
this.emptyRowHeight = this.shellHeight - approxChildrenHeight; this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
this.showEmptyRow = true; this.showEmptyRow = true;
} else { } else {
this.showEmptyRow = showEmptyRow;
this.showBottomShadow = true; this.showBottomShadow = true;
} }
}, },
...@@ -141,6 +136,28 @@ export default { ...@@ -141,6 +136,28 @@ export default {
scrollToTodayIndicator() { scrollToTodayIndicator() {
this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0); 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 isListVertScrollable =
this.$el.parentElement.offsetHeight < EPIC_ITEM_HEIGHT * (this.epics.length + 1);
// Toggle empty row.
this.showEmptyRow = !isListVertScrollable;
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) { handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight; this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
}, },
......
<script> <script>
import bp from '~/breakpoints'; import { GlSkeletonLoading } from '@gitlab/ui';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, SHELL_MIN_WIDTH, EXTEND_AS } from '../constants'; import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, EXTEND_AS } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import epicsListSection from './epics_list_section.vue'; import epicsListSection from './epics_list_section.vue';
...@@ -9,6 +10,7 @@ import roadmapTimelineSection from './roadmap_timeline_section.vue'; ...@@ -9,6 +10,7 @@ import roadmapTimelineSection from './roadmap_timeline_section.vue';
export default { export default {
components: { components: {
GlSkeletonLoading,
epicsListSection, epicsListSection,
roadmapTimelineSection, roadmapTimelineSection,
}, },
...@@ -40,18 +42,29 @@ export default { ...@@ -40,18 +42,29 @@ export default {
}, },
computed: { computed: {
containerStyles() { containerStyles() {
const width =
bp.windowWidth() > SHELL_MIN_WIDTH
? this.shellWidth + this.getWidthOffset()
: this.shellWidth;
return { return {
width: `${width}px`, width: `${this.shellWidth}px`,
height: `${this.shellHeight}px`, height: `${this.shellHeight}px`,
}; };
}, },
}, },
watch: {
/**
* Watcher to monitor whether epics list is long enough
* to allow vertical list scrolling.
*
* In case of scrollable list, we don't want vertical scrollbar
* to be visible, so we mask the scrollbar by increasing shell
* width past the scrollbar size.
*/
noScroll(value) {
if (this.$el.parentElement) {
this.shellWidth = this.getShellWidth(value);
}
},
},
mounted() { mounted() {
eventHub.$on('refreshTimeline', this.handleEpicsListRendered);
this.$nextTick(() => { this.$nextTick(() => {
// Client width at the time of component mount will not // Client width at the time of component mount will not
// provide accurate size of viewport until child contents are // provide accurate size of viewport until child contents are
...@@ -62,18 +75,28 @@ export default { ...@@ -62,18 +75,28 @@ export default {
if (this.$el.parentElement) { if (this.$el.parentElement) {
this.shellHeight = window.innerHeight - this.$el.offsetTop; this.shellHeight = window.innerHeight - this.$el.offsetTop;
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1); this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
this.shellWidth = this.$el.parentElement.clientWidth + this.getWidthOffset(); this.shellWidth = this.getShellWidth(this.noScroll);
this.timeframeStartOffset = this.$refs.roadmapTimeline.$el // We're guarding this as in tests, `roadmapTimeline`
.querySelector('.timeline-header-item') // is not ready when this line is executed.
.querySelector('.item-sublabel .sublabel-value:first-child') if (this.$refs.roadmapTimeline) {
.getBoundingClientRect().left; this.timeframeStartOffset = this.$refs.roadmapTimeline.$el
.querySelector('.timeline-header-item')
.querySelector('.item-sublabel .sublabel-value:first-child')
.getBoundingClientRect().left;
}
} }
}); });
}, },
beforeDestroy() {
eventHub.$off('refreshTimeline', this.handleEpicsListRendered);
},
methods: { methods: {
getWidthOffset() { getShellWidth(noScroll) {
return this.noScroll ? 0 : SCROLL_BAR_SIZE; return this.$el.parentElement.clientWidth + (noScroll ? 0 : SCROLL_BAR_SIZE);
},
handleEpicsListRendered() {
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
}, },
handleScroll() { handleScroll() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el; const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
...@@ -92,7 +115,6 @@ export default { ...@@ -92,7 +115,6 @@ export default {
this.$emit('onScrollToEnd', this.$refs.roadmapTimeline.$el, EXTEND_AS.APPEND); this.$emit('onScrollToEnd', this.$refs.roadmapTimeline.$el, EXTEND_AS.APPEND);
} }
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
eventHub.$emit('epicsListScrolled', { scrollTop, scrollLeft, clientHeight, scrollHeight }); eventHub.$emit('epicsListScrolled', { scrollTop, scrollLeft, clientHeight, scrollHeight });
}, },
}, },
...@@ -114,6 +136,11 @@ export default { ...@@ -114,6 +136,11 @@ export default {
:shell-width="shellWidth" :shell-width="shellWidth"
:list-scrollable="!noScroll" :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">
<gl-skeleton-loading :lines="2" />
</div>
</div>
<epics-list-section <epics-list-section
:preset-type="presetType" :preset-type="presetType"
:epics="epics" :epics="epics"
......
<script> <script>
import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility'; import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -42,8 +42,9 @@ export default { ...@@ -42,8 +42,9 @@ export default {
* and renders vertical line over the area where * and renders vertical line over the area where
* today falls in current timeline * today falls in current timeline
*/ */
handleEpicsListRender({ height, todayBarReady }) { handleEpicsListRender({ todayBarReady }) {
let left = 0; let left = 0;
let height = 0;
// Get total days of current timeframe Item and then // Get total days of current timeframe Item and then
// get size in % from current date and days in range // get size in % from current date and days in range
...@@ -59,15 +60,21 @@ export default { ...@@ -59,15 +60,21 @@ export default {
(this.currentDate.getDate() / totalDaysInMonth(this.timeframeItem)) * 100, (this.currentDate.getDate() / totalDaysInMonth(this.timeframeItem)) * 100,
); );
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetType === PRESET_TYPES.WEEKS) {
left = Math.floor(((this.currentDate.getDay() + 1) / 7) * 100 - 7); 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;
} }
// 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 = { this.todayBarStyles = {
height: `${height + 20}px`, height: `${height}px`,
left: `${left}%`, left: `${left}%`,
}; };
this.todayBarReady = todayBarReady === undefined ? true : todayBarReady; this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
......
...@@ -6,8 +6,6 @@ export const EPIC_ITEM_HEIGHT = 50; ...@@ -6,8 +6,6 @@ export const EPIC_ITEM_HEIGHT = 50;
export const TIMELINE_CELL_MIN_WIDTH = 180; export const TIMELINE_CELL_MIN_WIDTH = 180;
export const SHELL_MIN_WIDTH = 1620;
export const SCROLL_BAR_SIZE = 15; export const SCROLL_BAR_SIZE = 15;
export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000; export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000;
......
...@@ -204,7 +204,7 @@ export const getTimeframeForWeeksView = (initialDate = new Date(), length) => { ...@@ -204,7 +204,7 @@ export const getTimeframeForWeeksView = (initialDate = new Date(), length) => {
timeframe.push(newDate(startDate)); timeframe.push(newDate(startDate));
// Move date next Sunday // Move date next Sunday
startDate.setDate(startDate.getDate() + 7); startDate.setDate(startDate.getDate() + DAYS_IN_WEEK);
} }
return timeframe; return timeframe;
...@@ -216,12 +216,10 @@ export const extendTimeframeForWeeksView = (initialDate = new Date(), length) => ...@@ -216,12 +216,10 @@ export const extendTimeframeForWeeksView = (initialDate = new Date(), length) =>
if (length < 0) { if (length < 0) {
// When length is negative, we need to go // When length is negative, we need to go
// back as many weeks in time as value of length // back as many weeks in time as value of length
startDate.setDate(startDate.getDate() + (length + 1) * 7); startDate.setDate(startDate.getDate() + length * DAYS_IN_WEEK);
} else {
startDate.setDate(startDate.getDate());
} }
return getTimeframeForWeeksView(startDate, Math.abs(length) + 1); return getTimeframeForWeeksView(startDate, Math.abs(length));
}; };
export const extendTimeframeForPreset = ({ export const extendTimeframeForPreset = ({
...@@ -348,8 +346,8 @@ export const getTimeframeForPreset = ( ...@@ -348,8 +346,8 @@ export const getTimeframeForPreset = (
timeframe = getTimeframeForWeeksView(); timeframe = getTimeframeForWeeksView();
timeframeStart = newDate(timeframe[0]); timeframeStart = newDate(timeframe[0]);
timeframeEnd = newDate(timeframe[timeframe.length - 1]); timeframeEnd = newDate(timeframe[timeframe.length - 1]);
timeframeStart.setDate(timeframeStart.getDate() - 7); // Move date back by a week timeframeStart.setDate(timeframeStart.getDate());
timeframeEnd.setDate(timeframeEnd.getDate() + 7); // Move date ahead by a week timeframeEnd.setDate(timeframeEnd.getDate() + DAYS_IN_WEEK); // Move date ahead by a week
} }
// Extend timeframe on initial load to ensure // Extend timeframe on initial load to ensure
...@@ -413,30 +411,40 @@ export const getEpicsPathForPreset = ({ ...@@ -413,30 +411,40 @@ export const getEpicsPathForPreset = ({
export const sortEpics = (epics, sortedBy) => { export const sortEpics = (epics, sortedBy) => {
const sortByStartDate = sortedBy.indexOf('start_date') > -1; const sortByStartDate = sortedBy.indexOf('start_date') > -1;
const sortOrderAsc = sortedBy.indexOf('asc') > -1; const sortOrderAsc = sortedBy.indexOf('asc') > -1;
const pastDate = new Date(new Date().getFullYear() - 100, 0, 1);
const futureDate = new Date(new Date().getFullYear() + 100, 0, 1);
epics.sort((a, b) => { epics.sort((a, b) => {
let aDate; let aDate;
let bDate; let bDate;
if (sortByStartDate) { if (sortByStartDate) {
aDate = a.startDate; if (a.startDateUndefined) {
if (a.startDateOutOfRange) { // Set proxy date to be far in the past
aDate = a.originalStartDate; // to ensure sort order is correct.
aDate = pastDate;
} else {
aDate = a.startDateOutOfRange ? a.originalStartDate : a.startDate;
} }
bDate = b.startDate; if (b.startDateUndefined) {
if (b.startDateOutOfRange) { aDate = pastDate;
bDate = b.originalStartDate; } else {
bDate = b.startDateOutOfRange ? b.originalStartDate : b.startDate;
} }
} else { } else {
aDate = a.endDate; if (a.endDateUndefined) {
if (a.endDateOutOfRange) { // Set proxy date to be far into the future
aDate = a.originalEndDate; // to ensure sort order is correct.
aDate = futureDate;
} else {
aDate = a.endDateOutOfRange ? a.originalEndDate : a.endDate;
} }
bDate = b.endDate; if (b.endDateUndefined) {
if (b.endDateOutOfRange) { bDate = futureDate;
bDate = b.originalEndDate; } else {
bDate = b.endDateOutOfRange ? b.originalEndDate : b.endDate;
} }
} }
......
$header-item-height: 60px; $header-item-height: 60px;
$item-height: 50px; $item-height: 50px;
$details-cell-width: 320px; $details-cell-width: 320px;
$timeline-cell-width: 180px;
$border-style: 1px solid $border-gray-normal; $border-style: 1px solid $border-gray-normal;
$roadmap-gradient-dark-gray: rgba(0, 0, 0, 0.15); $roadmap-gradient-dark-gray: rgba(0, 0, 0, 0.15);
$roadmap-gradient-gray: rgba(255, 255, 255, 0.001); $roadmap-gradient-gray: rgba(255, 255, 255, 0.001);
...@@ -118,15 +119,32 @@ $column-right-gradient: linear-gradient( ...@@ -118,15 +119,32 @@ $column-right-gradient: linear-gradient(
} }
.roadmap-shell { .roadmap-shell {
position: relative;
overflow-x: auto; 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;
padding-left: $gl-padding;
z-index: 4;
&::after {
height: 100%;
}
}
&.prevent-vertical-scroll { &.prevent-vertical-scroll {
overflow-y: hidden; overflow-y: hidden;
} }
} }
.roadmap-timeline-section .timeline-header-blank::after, .roadmap-timeline-section .timeline-header-blank::after,
.epics-list-section .epic-details-cell::after { .epics-list-section .epic-details-cell::after,
.skeleton-loader::after {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
......
...@@ -66,7 +66,7 @@ describe('AppComponent', () => { ...@@ -66,7 +66,7 @@ describe('AppComponent', () => {
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
expect(vm.isLoading).toBe(true); expect(vm.isLoading).toBe(false);
expect(vm.isEpicsListEmpty).toBe(false); expect(vm.isEpicsListEmpty).toBe(false);
expect(vm.hasError).toBe(false); expect(vm.hasError).toBe(false);
expect(vm.handleResizeThrottled).toBeDefined(); expect(vm.handleResizeThrottled).toBeDefined();
...@@ -150,6 +150,7 @@ describe('AppComponent', () => { ...@@ -150,6 +150,7 @@ describe('AppComponent', () => {
it('calls service.getEpics and sets response to the store on success', done => { it('calls service.getEpics and sets response to the store on success', done => {
mock.onGet(vm.service.epicsPath).reply(200, rawEpics); mock.onGet(vm.service.epicsPath).reply(200, rawEpics);
spyOn(vm.store, 'setEpics'); spyOn(vm.store, 'setEpics');
spyOn(eventHub, '$emit');
vm.fetchEpics(); vm.fetchEpics();
...@@ -157,7 +158,19 @@ describe('AppComponent', () => { ...@@ -157,7 +158,19 @@ describe('AppComponent', () => {
setTimeout(() => { setTimeout(() => {
expect(vm.isLoading).toBe(false); expect(vm.isLoading).toBe(false);
expect(vm.store.setEpics).toHaveBeenCalledWith(rawEpics); expect(vm.store.setEpics).toHaveBeenCalledWith(rawEpics);
done();
vm.$nextTick()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith(
'refreshTimeline',
jasmine.objectContaining({
todayBarReady: true,
initialRender: true,
}),
);
})
.then(done)
.catch(done.fail);
}, 0); }, 0);
}); });
...@@ -210,13 +223,15 @@ describe('AppComponent', () => { ...@@ -210,13 +223,15 @@ describe('AppComponent', () => {
it('calls service.fetchEpicsForTimeframe and adds response to the store on success', done => { it('calls service.fetchEpicsForTimeframe and adds response to the store on success', done => {
mock.onGet(vm.service.epicsPath).reply(200, rawEpics); mock.onGet(vm.service.epicsPath).reply(200, rawEpics);
const extendType = EXTEND_AS.APPEND;
spyOn(vm.service, 'getEpicsForTimeframe').and.callThrough(); spyOn(vm.service, 'getEpicsForTimeframe').and.callThrough();
spyOn(vm.store, 'addEpics'); spyOn(vm.store, 'addEpics');
spyOn(vm, '$nextTick').and.stub(); spyOn(vm, 'processExtendedTimeline');
vm.fetchEpicsForTimeframe({ vm.fetchEpicsForTimeframe({
timeframe: mockTimeframeMonths, timeframe: mockTimeframeMonths,
extendType: EXTEND_AS.APPEND, extendType,
roadmapTimelineEl, roadmapTimelineEl,
}); });
...@@ -227,7 +242,18 @@ describe('AppComponent', () => { ...@@ -227,7 +242,18 @@ describe('AppComponent', () => {
); );
setTimeout(() => { setTimeout(() => {
expect(vm.store.addEpics).toHaveBeenCalledWith(rawEpics); expect(vm.store.addEpics).toHaveBeenCalledWith(rawEpics);
done(); vm.$nextTick()
.then(() => {
expect(vm.processExtendedTimeline).toHaveBeenCalledWith(
jasmine.objectContaining({
itemsCount: 8,
extendType,
roadmapTimelineEl,
}),
);
})
.then(done)
.catch(done.fail);
}, 0); }, 0);
}); });
...@@ -307,7 +333,6 @@ describe('AppComponent', () => { ...@@ -307,7 +333,6 @@ describe('AppComponent', () => {
it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `prepend`', done => { it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `prepend`', done => {
spyOn(vm.store, 'extendTimeframe'); spyOn(vm.store, 'extendTimeframe');
spyOn(vm, 'processExtendedTimeline');
spyOn(vm, 'fetchEpicsForTimeframe'); spyOn(vm, 'fetchEpicsForTimeframe');
const extendType = EXTEND_AS.PREPEND; const extendType = EXTEND_AS.PREPEND;
...@@ -319,14 +344,6 @@ describe('AppComponent', () => { ...@@ -319,14 +344,6 @@ describe('AppComponent', () => {
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(vm.processExtendedTimeline).toHaveBeenCalledWith(
jasmine.objectContaining({
itemsCount: 0,
extendType,
roadmapTimelineEl,
}),
);
expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith( expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith(
jasmine.objectContaining({ jasmine.objectContaining({
// During tests, we don't extend timeframe // During tests, we don't extend timeframe
...@@ -343,7 +360,6 @@ describe('AppComponent', () => { ...@@ -343,7 +360,6 @@ describe('AppComponent', () => {
it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `append`', done => { it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `append`', done => {
spyOn(vm.store, 'extendTimeframe'); spyOn(vm.store, 'extendTimeframe');
spyOn(vm, 'processExtendedTimeline');
spyOn(vm, 'fetchEpicsForTimeframe'); spyOn(vm, 'fetchEpicsForTimeframe');
const extendType = EXTEND_AS.APPEND; const extendType = EXTEND_AS.APPEND;
...@@ -355,14 +371,6 @@ describe('AppComponent', () => { ...@@ -355,14 +371,6 @@ describe('AppComponent', () => {
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(vm.processExtendedTimeline).toHaveBeenCalledWith(
jasmine.objectContaining({
itemsCount: 0,
extendType,
roadmapTimelineEl,
}),
);
expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith( expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith(
jasmine.objectContaining({ jasmine.objectContaining({
// During tests, we don't extend timeframe // During tests, we don't extend timeframe
......
...@@ -7,7 +7,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; ...@@ -7,7 +7,7 @@ import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants'; import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockEpic, mockTimeframeInitialDate, mockGroupId, mockScrollBarSize } from '../mock_data'; import { mockEpic, mockTimeframeInitialDate, mockGroupId } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -45,6 +45,7 @@ describe('RoadmapShellComponent', () => { ...@@ -45,6 +45,7 @@ describe('RoadmapShellComponent', () => {
expect(vm.shellWidth).toBe(0); expect(vm.shellWidth).toBe(0);
expect(vm.shellHeight).toBe(0); expect(vm.shellHeight).toBe(0);
expect(vm.noScroll).toBe(false); expect(vm.noScroll).toBe(false);
expect(vm.timeframeStartOffset).toBe(0);
}); });
}); });
...@@ -59,7 +60,7 @@ describe('RoadmapShellComponent', () => { ...@@ -59,7 +60,7 @@ describe('RoadmapShellComponent', () => {
document.querySelector('.roadmap-container').remove(); document.querySelector('.roadmap-container').remove();
}); });
it('returns style object based on shellWidth and current Window width with Scollbar size offset', done => { it('returns style object based on shellWidth and shellHeight', done => {
const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell')); const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell'));
Vue.nextTick(() => { Vue.nextTick(() => {
const stylesObj = vmWithParentEl.containerStyles; const stylesObj = vmWithParentEl.containerStyles;
...@@ -75,30 +76,16 @@ describe('RoadmapShellComponent', () => { ...@@ -75,30 +76,16 @@ describe('RoadmapShellComponent', () => {
}); });
describe('methods', () => { describe('methods', () => {
describe('getWidthOffset', () => { beforeEach(() => {
it('returns 0 when noScroll prop is true', () => { document.body.innerHTML +=
vm.noScroll = true; '<div class="roadmap-container"><div id="roadmap-shell"></div></div>';
});
expect(vm.getWidthOffset()).toBe(0);
});
it('returns value of SCROLL_BAR_SIZE when noScroll prop is false', () => {
vm.noScroll = false;
expect(vm.getWidthOffset()).toBe(mockScrollBarSize); afterEach(() => {
}); document.querySelector('.roadmap-container').remove();
}); });
describe('handleScroll', () => { describe('handleScroll', () => {
beforeEach(() => {
document.body.innerHTML +=
'<div class="roadmap-container"><div id="roadmap-shell"></div></div>';
});
afterEach(() => {
document.querySelector('.roadmap-container').remove();
});
it('emits `epicsListScrolled` event via eventHub', done => { it('emits `epicsListScrolled` event via eventHub', done => {
const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell')); const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell'));
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
...@@ -123,6 +110,17 @@ describe('RoadmapShellComponent', () => { ...@@ -123,6 +110,17 @@ describe('RoadmapShellComponent', () => {
expect(vm.$el.classList.contains('roadmap-shell')).toBe(true); expect(vm.$el.classList.contains('roadmap-shell')).toBe(true);
}); });
it('renders skeleton loader element when Epics list is empty', done => {
vm.epics = [];
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-skeleton-loader')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
it('adds `prevent-vertical-scroll` class on component container element', done => { it('adds `prevent-vertical-scroll` class on component container element', done => {
vm.noScroll = true; vm.noScroll = true;
Vue.nextTick(() => { Vue.nextTick(() => {
......
...@@ -49,17 +49,24 @@ describe('TimelineTodayIndicatorComponent', () => { ...@@ -49,17 +49,24 @@ describe('TimelineTodayIndicatorComponent', () => {
describe('methods', () => { describe('methods', () => {
describe('handleEpicsListRender', () => { describe('handleEpicsListRender', () => {
it('sets `todayBarStyles` and `todayBarReady` props based on provided height param, timeframeItem and currentDate props', () => { it('sets `todayBarStyles` and `todayBarReady` props', () => {
vm = createComponent({}); vm = createComponent({});
vm.handleEpicsListRender({ vm.handleEpicsListRender({});
height: 100,
});
const stylesObj = vm.todayBarStyles; const stylesObj = vm.todayBarStyles;
expect(stylesObj.height).toBe('120px'); expect(stylesObj.height).toBe('600px');
expect(stylesObj.left).toBe('50%'); expect(stylesObj.left).toBe('50%');
expect(vm.todayBarReady).toBe(true); expect(vm.todayBarReady).toBe(true);
}); });
it('sets `todayBarReady` prop based on value of provided `todayBarReady` param', () => {
vm = createComponent({});
vm.handleEpicsListRender({
todayBarReady: false,
});
expect(vm.todayBarReady).toBe(false);
});
}); });
}); });
...@@ -90,9 +97,7 @@ describe('TimelineTodayIndicatorComponent', () => { ...@@ -90,9 +97,7 @@ describe('TimelineTodayIndicatorComponent', () => {
describe('template', () => { describe('template', () => {
it('renders component container element with class `today-bar`', done => { it('renders component container element with class `today-bar`', done => {
vm = createComponent({}); vm = createComponent({});
vm.handleEpicsListRender({ vm.handleEpicsListRender({});
height: 100,
});
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.classList.contains('today-bar')).toBe(true); expect(vm.$el.classList.contains('today-bar')).toBe(true);
done(); done();
......
...@@ -70,13 +70,12 @@ export const mockTimeframeMonthsAppend = [ ...@@ -70,13 +70,12 @@ export const mockTimeframeMonthsAppend = [
]; ];
export const mockTimeframeWeeksPrepend = [ export const mockTimeframeWeeksPrepend = [
new Date(2017, 10, 5),
new Date(2017, 10, 12), new Date(2017, 10, 12),
new Date(2017, 10, 19), new Date(2017, 10, 19),
new Date(2017, 10, 26), new Date(2017, 10, 26),
new Date(2017, 11, 3), new Date(2017, 11, 3),
new Date(2017, 11, 10), new Date(2017, 11, 10),
new Date(2017, 11, 17),
new Date(2017, 11, 24),
]; ];
export const mockTimeframeWeeksAppend = [ export const mockTimeframeWeeksAppend = [
new Date(2018, 0, 28), new Date(2018, 0, 28),
...@@ -85,7 +84,6 @@ export const mockTimeframeWeeksAppend = [ ...@@ -85,7 +84,6 @@ export const mockTimeframeWeeksAppend = [
new Date(2018, 1, 18), new Date(2018, 1, 18),
new Date(2018, 1, 25), new Date(2018, 1, 25),
new Date(2018, 2, 4), new Date(2018, 2, 4),
new Date(2018, 2, 11),
]; ];
export const mockEpic = { export const mockEpic = {
......
...@@ -4529,9 +4529,6 @@ msgstr "" ...@@ -4529,9 +4529,6 @@ msgstr ""
msgid "GroupRoadmap|From %{dateWord}" msgid "GroupRoadmap|From %{dateWord}"
msgstr "" msgstr ""
msgid "GroupRoadmap|Loading roadmap"
msgstr ""
msgid "GroupRoadmap|Something went wrong while fetching epics" msgid "GroupRoadmap|Something went wrong while fetching epics"
msgstr "" msgstr ""
......
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