Skip to content
Snippets Groups Projects
Commit 71677b3e authored by Fatih Acet's avatar Fatih Acet
Browse files

Merge branch '7325-roadmap-infinite-scrolling' into 'master'

Add support for auto-expanding Roadmap timeline on horizontal scroll

Closes #9119 and #7325

See merge request gitlab-org/gitlab-ee!9018
parents e5b90202 33acd91e
No related branches found
No related tags found
No related merge requests found
Showing
with 725 additions and 333 deletions
Loading
Loading
@@ -6,6 +6,9 @@ import { s__ } from '~/locale';
import { GlLoadingIcon } from '@gitlab/ui';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
import eventHub from '../event_hub';
import { EXTEND_AS } from '../constants';
 
export default {
components: {
Loading
Loading
@@ -96,6 +99,28 @@ export default {
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
},
fetchEpicsForTimeframe({ timeframe, roadmapTimelineEl, extendType }) {
this.hasError = false;
this.service
.getEpicsForTimeframe(this.presetType, timeframe)
.then(res => res.data)
.then(epics => {
if (epics.length) {
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,
});
});
}
})
.catch(() => {
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
},
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
Loading
Loading
@@ -117,6 +142,49 @@ export default {
this.isLoading = false;
}, 200)();
},
/**
* Once timeline is expanded (either with prepend or append)
* We need performing following actions;
*
* 1. Reset start and end edges of the timeline for
* infinite scrolling to continue further.
* 2. Re-render timeline bars to account for
* updated timeframe.
* 3. In case of prepending timeframe,
* reset scroll-position (due to DOM prepend).
*/
processExtendedTimeline({ extendType = EXTEND_AS.PREPEND, roadmapTimelineEl, itemsCount = 0 }) {
// Re-render timeline bars with updated timeline
eventHub.$emit('refreshTimeline', {
height: window.innerHeight - roadmapTimelineEl.offsetTop,
todayBarReady: extendType === EXTEND_AS.PREPEND,
});
if (extendType === EXTEND_AS.PREPEND) {
// When DOM is prepended with elements
// we compensate the scrolling for added elements' width
roadmapTimelineEl.parentElement.scrollBy(
roadmapTimelineEl.querySelector('.timeline-header-item').clientWidth * itemsCount,
0,
);
}
},
handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) {
const timeframe = this.store.extendTimeframe(extendType);
this.$nextTick(() => {
this.processExtendedTimeline({
itemsCount: timeframe ? timeframe.length : 0,
extendType,
roadmapTimelineEl,
});
this.fetchEpicsForTimeframe({
timeframe,
roadmapTimelineEl,
extendType,
});
});
},
},
};
</script>
Loading
Loading
@@ -135,6 +203,8 @@ export default {
:epics="epics"
:timeframe="timeframe"
:current-group-id="currentGroupId"
@onScrollToStart="handleScrollToExtend"
@onScrollToEnd="handleScrollToExtend"
/>
<epics-list-empty
v-if="isEpicsListEmpty"
Loading
Loading
<script>
import _ from 'underscore';
import epicItemDetails from './epic_item_details.vue';
import epicItemTimeline from './epic_item_timeline.vue';
 
import { EPIC_HIGHLIGHT_REMOVE_AFTER } from '../constants';
export default {
components: {
epicItemDetails,
Loading
Loading
@@ -33,11 +37,36 @@ export default {
required: true,
},
},
updated() {
this.removeHighlight();
},
methods: {
/**
* When new epics are added to the list on
* timeline scroll, we set `newEpic` flag
* as true and then use it in template
* to set `newly-added-epic` class for
* highlighting epic using CSS animations
*
* Once animation is complete, we need to
* remove the flag so that animation is not
* replayed when list is re-rendered.
*/
removeHighlight() {
if (this.epic.newEpic) {
this.$nextTick(() => {
_.delay(() => {
this.epic.newEpic = false;
}, EPIC_HIGHLIGHT_REMOVE_AFTER);
});
}
},
},
};
</script>
 
<template>
<div class="epics-list-item clearfix">
<div :class="{ 'newly-added-epic': epic.newEpic }" class="epics-list-item clearfix">
<epic-item-details :epic="epic" :current-group-id="currentGroupId" />
<epic-item-timeline
v-for="(timeframeItem, index) in timeframe"
Loading
Loading
Loading
Loading
@@ -5,13 +5,9 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
 
import {
EPIC_DETAILS_CELL_WIDTH,
TIMELINE_CELL_MIN_WIDTH,
TIMELINE_END_OFFSET_FULL,
TIMELINE_END_OFFSET_HALF,
PRESET_TYPES,
} from '../constants';
import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
 
export default {
directives: {
Loading
Loading
@@ -66,6 +62,12 @@ export default {
this.renderTimelineBar();
},
},
mounted() {
eventHub.$on('refreshTimeline', this.renderTimelineBar);
},
beforeDestroy() {
eventHub.$off('refreshTimeline', this.renderTimelineBar);
},
methods: {
/**
* Gets cell width based on total number months for
Loading
Loading
@@ -89,49 +91,6 @@ export default {
}
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 (
(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;
}
return offset;
},
/**
* Renders timeline bar only if current
* timeframe item has startDate for the epic.
Loading
Loading
@@ -160,9 +119,7 @@ export default {
:href="epic.webUrl"
:class="{
'start-date-undefined': epic.startDateUndefined,
'start-date-outside': epic.startDateOutOfRange,
'end-date-undefined': epic.endDateUndefined,
'end-date-outside': epic.endDateOutOfRange,
}"
:style="timelineBarStyles"
class="timeline-bar"
Loading
Loading
Loading
Loading
@@ -2,7 +2,7 @@
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
 
import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
import { PRESET_TYPES, emptyStateDefault, emptyStateWithFilters } from '../constants';
 
import NewEpic from '../../epics/new_epic/components/new_epic.vue';
 
Loading
Loading
@@ -82,12 +82,12 @@ export default {
},
subMessage() {
if (this.hasFiltersApplied) {
return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateWithFilters, {
return sprintf(emptyStateWithFilters, {
startDate: this.timeframeRange.startDate,
endDate: this.timeframeRange.endDate,
});
}
return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateDefault, {
return sprintf(emptyStateDefault, {
startDate: this.timeframeRange.startDate,
endDate: this.timeframeRange.endDate,
});
Loading
Loading
Loading
Loading
@@ -3,6 +3,8 @@ import eventHub from '../event_hub';
 
import SectionMixin from '../mixins/section_mixin';
 
import { TIMELINE_CELL_MIN_WIDTH } from '../constants';
import epicItem from './epic_item.vue';
 
export default {
Loading
Loading
@@ -72,12 +74,18 @@ export default {
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', () => {
this.initEmptyRow(false);
});
this.$nextTick(() => {
this.initMounted();
});
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', () => {
this.initEmptyRow(false);
});
},
methods: {
initMounted() {
Loading
Loading
@@ -104,7 +112,7 @@ export default {
* based on height of available list items and sets it to component
* props.
*/
initEmptyRow() {
initEmptyRow(showEmptyRow = false) {
const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
 
Loading
Loading
@@ -122,18 +130,16 @@ export default {
this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
this.showEmptyRow = true;
} else {
this.showEmptyRow = showEmptyRow;
this.showBottomShadow = true;
}
},
/**
* `clientWidth` is full width of list section, and we need to
* scroll up to 60% of the view where today indicator is present.
*
* Reason for 60% is that "today" always falls in the middle of timeframe range.
* Scroll timeframe to the right of the timeline
* by half the column size
*/
scrollToTodayIndicator() {
const uptoTodayIndicator = Math.ceil((this.$el.clientWidth * 60) / 100);
this.$el.scrollTo(uptoTodayIndicator, 0);
this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
Loading
Loading
Loading
Loading
@@ -29,8 +29,6 @@ export default {
 
return {
currentDate,
quarterBeginDate: this.timeframeItem.range[0],
quarterEndDate: this.timeframeItem.range[2],
};
},
computed: {
Loading
Loading
@@ -39,6 +37,12 @@ export default {
width: `${this.itemWidth}px`,
};
},
quarterBeginDate() {
return this.timeframeItem.range[0];
},
quarterEndDate() {
return this.timeframeItem.range[2];
},
timelineHeaderLabel() {
const { quarterSequence } = this.timeframeItem;
if (quarterSequence === 1 || (this.timeframeIndex === 0 && quarterSequence !== 1)) {
Loading
Loading
Loading
Loading
@@ -20,13 +20,13 @@ export default {
required: true,
},
},
data() {
return {
quarterBeginDate: this.timeframeItem.range[0],
quarterEndDate: this.timeframeItem.range[2],
};
},
computed: {
quarterBeginDate() {
return this.timeframeItem.range[0];
},
quarterEndDate() {
return this.timeframeItem.range[2];
},
headerSubItems() {
return this.timeframeItem.range;
},
Loading
Loading
Loading
Loading
@@ -29,12 +29,8 @@ export default {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
 
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
return {
currentDate,
lastDayOfCurrentWeek,
};
},
computed: {
Loading
Loading
@@ -43,19 +39,35 @@ export default {
width: `${this.itemWidth}px`,
};
},
lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
return lastDayOfCurrentWeek;
},
timelineHeaderLabel() {
if (this.timeframeIndex === 0) {
const timeframeItemMonth = this.timeframeItem.getMonth();
const timeframeItemDate = this.timeframeItem.getDate();
if (this.timeframeIndex === 0 || (timeframeItemMonth === 0 && timeframeItemDate <= 7)) {
return `${this.timeframeItem.getFullYear()} ${monthInWords(
this.timeframeItem,
true,
)} ${this.timeframeItem.getDate()}`;
)} ${timeframeItemDate}`;
}
return `${monthInWords(this.timeframeItem, true)} ${this.timeframeItem.getDate()}`;
return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
},
timelineHeaderClass() {
if (this.currentDate >= this.timeframeItem && this.currentDate <= this.lastDayOfCurrentWeek) {
const currentDateTime = this.currentDate.getTime();
const lastDayOfCurrentWeekTime = this.lastDayOfCurrentWeek.getTime();
if (
currentDateTime >= this.timeframeItem.getTime() &&
currentDateTime <= lastDayOfCurrentWeekTime
) {
return 'label-dark label-bold';
} else if (this.currentDate < this.lastDayOfCurrentWeek) {
} else if (currentDateTime < lastDayOfCurrentWeekTime) {
return 'label-dark';
}
return '';
Loading
Loading
Loading
Loading
@@ -18,28 +18,26 @@ export default {
required: true,
},
},
data() {
const timeframeItem = new Date(this.timeframeItem.getTime());
const headerSubItems = new Array(7)
.fill()
.map(
(val, i) =>
new Date(
timeframeItem.getFullYear(),
timeframeItem.getMonth(),
timeframeItem.getDate() + i,
),
);
return {
headerSubItems,
};
},
computed: {
headerSubItems() {
const timeframeItem = new Date(this.timeframeItem.getTime());
const headerSubItems = new Array(7)
.fill()
.map(
(val, i) =>
new Date(
timeframeItem.getFullYear(),
timeframeItem.getMonth(),
timeframeItem.getDate() + i,
),
);
return headerSubItems;
},
hasToday() {
return (
this.currentDate >= this.headerSubItems[0] &&
this.currentDate <= this.headerSubItems[this.headerSubItems.length - 1]
this.currentDate.getTime() >= this.headerSubItems[0].getTime() &&
this.currentDate.getTime() <= this.headerSubItems[this.headerSubItems.length - 1].getTime()
);
},
},
Loading
Loading
<script>
import bp from '~/breakpoints';
import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, SHELL_MIN_WIDTH } from '../constants';
import { isInViewport } from '~/lib/utils/common_utils';
import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, SHELL_MIN_WIDTH, EXTEND_AS } from '../constants';
import eventHub from '../event_hub';
 
import epicsListSection from './epics_list_section.vue';
Loading
Loading
@@ -34,6 +35,7 @@ export default {
shellWidth: 0,
shellHeight: 0,
noScroll: false,
timeframeStartOffset: 0,
};
},
computed: {
Loading
Loading
@@ -61,6 +63,11 @@ export default {
this.shellHeight = window.innerHeight - this.$el.offsetTop;
this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
this.shellWidth = this.$el.parentElement.clientWidth + this.getWidthOffset();
this.timeframeStartOffset = this.$refs.roadmapTimeline.$el
.querySelector('.timeline-header-item')
.querySelector('.item-sublabel .sublabel-value:first-child')
.getBoundingClientRect().left;
}
});
},
Loading
Loading
@@ -70,6 +77,22 @@ export default {
},
handleScroll() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
.querySelector('.timeline-header-item')
.querySelector('.item-sublabel .sublabel-value:first-child');
const timelineEdgeEndEl = this.$refs.roadmapTimeline.$el
.querySelector('.timeline-header-item:last-child')
.querySelector('.item-sublabel .sublabel-value:last-child');
// If timeline was scrolled to start
if (isInViewport(timelineEdgeStartEl, { left: this.timeframeStartOffset })) {
this.$emit('onScrollToStart', this.$refs.roadmapTimeline.$el, EXTEND_AS.PREPEND);
} else if (isInViewport(timelineEdgeEndEl)) {
// If timeline was scrolled to end
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 });
},
},
Loading
Loading
@@ -84,6 +107,7 @@ export default {
@scroll="handleScroll"
>
<roadmap-timeline-section
ref="roadmapTimeline"
:preset-type="presetType"
:epics="epics"
:timeframe="timeframe"
Loading
Loading
Loading
Loading
@@ -29,10 +29,12 @@ export default {
mounted() {
eventHub.$on('epicsListRendered', this.handleEpicsListRender);
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$on('refreshTimeline', this.handleEpicsListRender);
},
beforeDestroy() {
eventHub.$off('epicsListRendered', this.handleEpicsListRender);
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
eventHub.$off('refreshTimeline', this.handleEpicsListRender);
},
methods: {
/**
Loading
Loading
@@ -40,7 +42,7 @@ export default {
* and renders vertical line over the area where
* today falls in current timeline
*/
handleEpicsListRender({ height }) {
handleEpicsListRender({ height, todayBarReady }) {
let left = 0;
 
// Get total days of current timeframe Item and then
Loading
Loading
@@ -68,7 +70,7 @@ export default {
height: `${height + 20}px`,
left: `${left}%`,
};
this.todayBarReady = true;
this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
},
handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x;
Loading
Loading
import { s__ } from '~/locale';
 
export const TIMEFRAME_LENGTH = 6;
export const EPIC_DETAILS_CELL_WIDTH = 320;
 
export const EPIC_ITEM_HEIGHT = 50;
Loading
Loading
@@ -12,9 +10,7 @@ export const SHELL_MIN_WIDTH = 1620;
 
export const SCROLL_BAR_SIZE = 15;
 
export const TIMELINE_END_OFFSET_HALF = 8;
export const TIMELINE_END_OFFSET_FULL = 16;
export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000;
 
export const PRESET_TYPES = {
QUARTERS: 'QUARTERS',
Loading
Loading
@@ -22,32 +18,27 @@ export const PRESET_TYPES = {
WEEKS: 'WEEKS',
};
 
export const EXTEND_AS = {
PREPEND: 'prepend',
APPEND: 'append',
};
export const emptyStateDefault = s__(
'GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from %{startDate} to %{endDate}.',
);
export const emptyStateWithFilters = s__(
'GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}.',
);
export const PRESET_DEFAULTS = {
QUARTERS: {
TIMEFRAME_LENGTH: 18,
emptyStateDefault: s__(
'GroupRoadmap|To view the roadmap, add a start or due 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 %{startDate} to %{endDate}.',
),
emptyStateWithFilters: s__(
'GroupRoadmap|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 %{startDate} to %{endDate}.',
),
TIMEFRAME_LENGTH: 21,
},
MONTHS: {
TIMEFRAME_LENGTH: 7,
emptyStateDefault: s__(
'GroupRoadmap|To view the roadmap, add a start or due 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 %{startDate} to %{endDate}.',
),
emptyStateWithFilters: s__(
'GroupRoadmap|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 %{startDate} to %{endDate}.',
),
TIMEFRAME_LENGTH: 8,
},
WEEKS: {
TIMEFRAME_LENGTH: 42,
emptyStateDefault: s__(
'GroupRoadmap|To view the roadmap, add a start or due 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 %{startDate} to %{endDate}.',
),
emptyStateWithFilters: s__(
'GroupRoadmap|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 %{startDate} to %{endDate}.',
),
TIMEFRAME_LENGTH: 7,
},
};
Loading
Loading
@@ -5,7 +5,7 @@ import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
import { visitUrl, mergeUrlParams } from '~/lib/utils/url_utility';
 
import { PRESET_TYPES } from './constants';
import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants';
 
import { getTimeframeForPreset, getEpicsPathForPreset } from './utils/roadmap_utils';
 
Loading
Loading
@@ -48,23 +48,38 @@ export default () => {
? dataset.presetType
: PRESET_TYPES.MONTHS;
const filterQueryString = window.location.search.substring(1);
const timeframe = getTimeframeForPreset(presetType);
const epicsPath = getEpicsPathForPreset({
const timeframe = getTimeframeForPreset(
presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
);
const initialEpicsPath = getEpicsPathForPreset({
basePath: dataset.epicsPath,
epicsState: dataset.epicsState,
filterQueryString,
presetType,
timeframe,
state: dataset.epicsState,
});
 
const store = new RoadmapStore(parseInt(dataset.groupId, 0), timeframe, presetType);
const service = new RoadmapService(epicsPath);
const store = new RoadmapStore({
groupId: parseInt(dataset.groupId, 0),
sortedBy: dataset.sortedBy,
timeframe,
presetType,
});
const service = new RoadmapService({
initialEpicsPath,
filterQueryString,
basePath: dataset.epicsPath,
epicsState: dataset.epicsState,
});
 
return {
store,
service,
presetType,
hasFiltersApplied,
epicsState: dataset.epicsState,
newEpicEndpoint: dataset.newEpicEndpoint,
emptyStateIllustrationPath: dataset.emptyStateIllustration,
};
Loading
Loading
@@ -76,6 +91,7 @@ export default () => {
service: this.service,
presetType: this.presetType,
hasFiltersApplied: this.hasFiltersApplied,
epicsState: this.epicsState,
newEpicEndpoint: this.newEpicEndpoint,
emptyStateIllustrationPath: this.emptyStateIllustrationPath,
},
Loading
Loading
import { totalDaysInMonth } from '~/lib/utils/datetime_utility';
 
import { TIMELINE_END_OFFSET_HALF } from '../constants';
export default {
methods: {
/**
Loading
Loading
@@ -17,10 +15,10 @@ export default {
* Check if current epic ends within current month (timeline cell)
*/
isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate) {
return (
timeframeItem.getYear() <= epicEndDate.getYear() &&
timeframeItem.getMonth() === epicEndDate.getMonth()
);
if (epicEndDate.getFullYear() <= timeframeItem.getFullYear()) {
return epicEndDate.getMonth() === timeframeItem.getMonth();
}
return epicEndDate.getTime() < timeframeItem.getTime();
},
/**
* Return timeline bar width for current month (timeline cell) based on
Loading
Loading
@@ -61,16 +59,6 @@ export default {
return 'left: 0;';
}
 
// 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
return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
}
// Calculate proportional offset based on startDate and total days in
// current month.
return `left: ${(startDate / daysInMonth) * 100}%;`;
Loading
Loading
@@ -99,7 +87,6 @@ export default {
 
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;
 
Loading
Loading
@@ -149,8 +136,7 @@ export default {
}
}
 
// Reduce any offset from total width and round it off.
return timelineBarWidth - offsetEnd;
return timelineBarWidth;
},
},
};
import { totalDaysInQuarter, dayInQuarter } from '~/lib/utils/datetime_utility';
 
import { TIMELINE_END_OFFSET_HALF } from '../constants';
export default {
methods: {
/**
Loading
Loading
@@ -11,7 +9,10 @@ export default {
const quarterStart = this.timeframeItem.range[0];
const quarterEnd = this.timeframeItem.range[2];
 
return this.epic.startDate >= quarterStart && this.epic.startDate <= quarterEnd;
return (
this.epic.startDate.getTime() >= quarterStart.getTime() &&
this.epic.startDate.getTime() <= quarterEnd.getTime()
);
},
/**
* Check if current epic ends within current quarter (timeline cell)
Loading
Loading
@@ -19,7 +20,7 @@ export default {
isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate) {
const quarterEnd = timeframeItem.range[2];
 
return epicEndDate <= quarterEnd;
return epicEndDate.getTime() <= quarterEnd.getTime();
},
/**
* Return timeline bar width for current quarter (timeline cell) based on
Loading
Loading
@@ -57,14 +58,6 @@ export default {
return 'left: 0;';
}
 
const lastTimeframeItem = this.timeframe[this.timeframe.length - 1].range[2];
if (
this.epic.startDate >= this.timeframe[this.timeframe.length - 1].range[0] &&
this.epic.startDate <= lastTimeframeItem
) {
return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
}
return `left: ${(startDay / daysInQuarter) * 100}%;`;
},
/**
Loading
Loading
@@ -95,7 +88,6 @@ export default {
 
const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const offsetEnd = this.getTimelineBarEndOffset();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
 
Loading
Loading
@@ -140,7 +132,7 @@ export default {
}
}
 
return timelineBarWidth - offsetEnd;
return timelineBarWidth;
},
},
};
import { TIMELINE_END_OFFSET_HALF } from '../constants';
import { newDate } from '~/lib/utils/datetime_utility';
 
export default {
methods: {
Loading
Loading
@@ -7,16 +7,19 @@ export default {
*/
hasStartDateForWeek() {
const firstDayOfWeek = this.timeframeItem;
const lastDayOfWeek = new Date(this.timeframeItem.getTime());
const lastDayOfWeek = newDate(this.timeframeItem);
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
 
return this.epic.startDate >= firstDayOfWeek && this.epic.startDate <= lastDayOfWeek;
return (
this.epic.startDate.getTime() >= firstDayOfWeek.getTime() &&
this.epic.startDate.getTime() <= lastDayOfWeek.getTime()
);
},
/**
* Return last date of the week from provided timeframeItem
*/
getLastDayOfWeek(timeframeItem) {
const lastDayOfWeek = new Date(timeframeItem.getTime());
const lastDayOfWeek = newDate(timeframeItem);
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
return lastDayOfWeek;
},
Loading
Loading
@@ -25,7 +28,7 @@ export default {
*/
isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate) {
const lastDayOfWeek = this.getLastDayOfWeek(timeframeItem);
return epicEndDate <= lastDayOfWeek;
return epicEndDate.getTime() <= lastDayOfWeek.getTime();
},
/**
* Return timeline bar width for current week (timeline cell) based on
Loading
Loading
@@ -37,14 +40,6 @@ export default {
 
return Math.min(cellWidth, barWidth);
},
/**
* Gets timelinebar end offset based width of single day
* and TIMELINE_END_OFFSET_HALF
*/
getTimelineBarEndOffsetHalfForWeek() {
const dayWidth = this.getCellWidth() / 7;
return TIMELINE_END_OFFSET_HALF + dayWidth * 0.5;
},
/**
* In case startDate for any epic is undefined or is out of range
* for current timeframe, we have to provide specific offset while
Loading
Loading
@@ -73,15 +68,6 @@ export default {
return 'left: 0;';
}
 
const lastTimeframeItem = new Date(this.timeframe[this.timeframe.length - 1].getTime());
lastTimeframeItem.setDate(lastTimeframeItem.getDate() + 6);
if (
this.epic.startDate >= this.timeframe[this.timeframe.length - 1] &&
this.epic.startDate <= lastTimeframeItem
) {
return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
}
return `left: ${startDate * dayWidth - dayWidth / 2}px;`;
},
/**
Loading
Loading
@@ -111,7 +97,6 @@ export default {
 
const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem);
const cellWidth = this.getCellWidth();
const offsetEnd = this.getTimelineBarEndOffset();
const epicStartDate = this.epic.startDate;
const epicEndDate = this.epic.endDate;
 
Loading
Loading
@@ -135,7 +120,7 @@ export default {
}
}
 
return timelineBarWidth - offsetEnd;
return timelineBarWidth;
},
},
};
import axios from '~/lib/utils/axios_utils';
 
import { getEpicsPathForPreset } from '../utils/roadmap_utils';
export default class RoadmapService {
constructor(epicsPath) {
this.epicsPath = epicsPath;
constructor({ basePath, epicsState, filterQueryString, initialEpicsPath }) {
this.basePath = basePath;
this.epicsState = epicsState;
this.filterQueryString = filterQueryString;
this.initialEpicsPath = initialEpicsPath;
}
 
getEpics() {
return axios.get(this.epicsPath);
return axios.get(this.initialEpicsPath);
}
getEpicsForTimeframe(presetType, timeframe) {
const epicsPath = getEpicsPathForPreset({
basePath: this.basePath,
epicsState: this.epicsState,
filterQueryString: this.filterQueryString,
presetType,
timeframe,
});
return axios.get(epicsPath);
}
}
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import { newDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
 
import { PRESET_TYPES } from '../constants';
import { extendTimeframeForPreset, sortEpics } from '../utils/roadmap_utils';
import { PRESET_TYPES, EXTEND_AS } from '../constants';
 
export default class RoadmapStore {
constructor(groupId, timeframe, presetType) {
constructor({ groupId, timeframe, presetType, sortedBy }) {
this.state = {};
this.state.epics = [];
this.state.epicIds = [];
this.state.currentGroupId = groupId;
this.state.timeframe = timeframe;
 
this.presetType = presetType;
this.sortedBy = sortedBy;
this.initTimeframeThreshold();
}
 
Loading
Loading
@@ -27,24 +30,33 @@ export default class RoadmapStore {
this.timeframeEndDate = this.state.timeframe[lastTimeframeIndex];
} else if (this.presetType === PRESET_TYPES.WEEKS) {
this.timeframeStartDate = startFrame;
this.timeframeEndDate = new Date(this.state.timeframe[lastTimeframeIndex].getTime());
this.timeframeEndDate = newDate(this.state.timeframe[lastTimeframeIndex]);
this.timeframeEndDate.setDate(this.timeframeEndDate.getDate() + 7);
}
}
 
setEpics(epics) {
this.state.epics = epics.reduce((filteredEpics, epic) => {
const formattedEpic = RoadmapStore.formatEpicDetails(
epic,
this.timeframeStartDate,
this.timeframeEndDate,
);
// Exclude any Epic that has invalid dates
if (formattedEpic.startDate <= formattedEpic.endDate) {
filteredEpics.push(formattedEpic);
}
return filteredEpics;
}, []);
this.state.epicIds = [];
this.state.epics = RoadmapStore.filterInvalidEpics({
timeframeStartDate: this.timeframeStartDate,
timeframeEndDate: this.timeframeEndDate,
state: this.state,
epics,
});
}
addEpics(epics) {
this.state.epics = this.state.epics.concat(
RoadmapStore.filterInvalidEpics({
timeframeStartDate: this.timeframeStartDate,
timeframeEndDate: this.timeframeEndDate,
state: this.state,
newEpic: true,
epics,
}),
);
sortEpics(this.state.epics, this.sortedBy);
}
 
getEpics() {
Loading
Loading
@@ -59,6 +71,57 @@ export default class RoadmapStore {
return this.state.timeframe;
}
 
extendTimeframe(extendAs = EXTEND_AS.PREPEND) {
const timeframeToExtend = extendTimeframeForPreset({
presetType: this.presetType,
extendAs,
initialDate: extendAs === EXTEND_AS.PREPEND ? this.timeframeStartDate : this.timeframeEndDate,
});
if (extendAs === EXTEND_AS.PREPEND) {
this.state.timeframe.unshift(...timeframeToExtend);
} else {
this.state.timeframe.push(...timeframeToExtend);
}
this.initTimeframeThreshold();
this.state.epics.forEach(epic =>
RoadmapStore.processEpicDates(epic, this.timeframeStartDate, this.timeframeEndDate),
);
return timeframeToExtend;
}
static filterInvalidEpics({
epics,
timeframeStartDate,
timeframeEndDate,
state,
newEpic = false,
}) {
return epics.reduce((filteredEpics, epic) => {
const formattedEpic = RoadmapStore.formatEpicDetails(
epic,
timeframeStartDate,
timeframeEndDate,
);
// Exclude any Epic that has invalid dates
// or is already present in Roadmap timeline
if (
formattedEpic.startDate <= formattedEpic.endDate &&
state.epicIds.indexOf(formattedEpic.id) < 0
) {
Object.assign(formattedEpic, {
newEpic,
});
filteredEpics.push(formattedEpic);
state.epicIds.push(formattedEpic.id);
}
return filteredEpics;
}, []);
}
/**
* This method constructs Epic object and assigns proxy dates
* in case start or end dates are unavailable.
Loading
Loading
@@ -73,44 +136,75 @@ export default class RoadmapStore {
if (rawEpic.start_date) {
// If startDate is present
const startDate = parsePikadayDate(rawEpic.start_date);
if (startDate <= timeframeStartDate) {
// If startDate is less than first timeframe item
// startDate is out of range;
epicItem.startDateOutOfRange = true;
// store original start date in different object
epicItem.originalStartDate = startDate;
// Use startDate object to set a proxy date so
// that timeline bar can render it.
epicItem.startDate = new Date(timeframeStartDate.getTime());
} else {
// startDate is within timeframe range
epicItem.startDate = startDate;
}
epicItem.startDate = startDate;
epicItem.originalStartDate = startDate;
} else {
// Start date is not available
// startDate is not available
epicItem.startDateUndefined = true;
// Set proxy date so that timeline bar can render it.
epicItem.startDate = new Date(timeframeStartDate.getTime());
}
 
// Same as above but for endDate
// This entire chunk can be moved into generic method
// but we're keeping it here for the sake of simplicity.
if (rawEpic.end_date) {
// If endDate is present
const endDate = parsePikadayDate(rawEpic.end_date);
if (endDate >= timeframeEndDate) {
epicItem.endDateOutOfRange = true;
epicItem.originalEndDate = endDate;
epicItem.endDate = new Date(timeframeEndDate.getTime());
} else {
epicItem.endDate = endDate;
}
epicItem.endDate = endDate;
epicItem.originalEndDate = endDate;
} else {
// endDate is not available
epicItem.endDateUndefined = true;
epicItem.endDate = new Date(timeframeEndDate.getTime());
}
 
RoadmapStore.processEpicDates(epicItem, timeframeStartDate, timeframeEndDate);
return epicItem;
}
static processEpicDates(epic, timeframeStartDate, timeframeEndDate) {
if (!epic.startDateUndefined) {
// If startDate is less than first timeframe item
if (epic.originalStartDate.getTime() < timeframeStartDate.getTime()) {
Object.assign(epic, {
// startDate is out of range
startDateOutOfRange: true,
// Use startDate object to set a proxy date so
// that timeline bar can render it.
startDate: newDate(timeframeStartDate),
});
} else {
Object.assign(epic, {
// startDate is within range
startDateOutOfRange: false,
// Set startDate to original startDate
startDate: newDate(epic.originalStartDate),
});
}
} else {
Object.assign(epic, {
startDate: newDate(timeframeStartDate),
});
}
if (!epic.endDateUndefined) {
// If endDate is greater than last timeframe item
if (epic.originalEndDate.getTime() > timeframeEndDate.getTime()) {
Object.assign(epic, {
// endDate is out of range
endDateOutOfRange: true,
// Use endDate object to set a proxy date so
// that timeline bar can render it.
endDate: newDate(timeframeEndDate),
});
} else {
Object.assign(epic, {
// startDate is within range
endDateOutOfRange: false,
// Set startDate to original startDate
endDate: newDate(epic.originalEndDate),
});
}
} else {
Object.assign(epic, {
endDate: newDate(timeframeEndDate),
});
}
}
}
import { getTimeframeWindowFrom, totalDaysInMonth } from '~/lib/utils/datetime_utility';
import { newDate, getTimeframeWindowFrom, totalDaysInMonth } from '~/lib/utils/datetime_utility';
 
import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
import { PRESET_TYPES, PRESET_DEFAULTS, EXTEND_AS, TIMELINE_CELL_MIN_WIDTH } from '../constants';
const monthsForQuarters = {
1: [0, 1, 2],
2: [3, 4, 5],
3: [6, 7, 8],
4: [9, 10, 11],
};
 
/**
* This method returns array of Objects representing Quarters based on provided initialDate
*
* For eg; If initialDate is 15th Jan 2018
* Then as per Roadmap specs, we need to show
* 1 quarter before current quarter AND
* 2 quarters before current quarters
* current quarter AND
* 4 quarters after current quarter
* thus, total of 6 quarters.
* thus, total of 7 quarters (21 Months).
*
* So returned array from this method will be;
* [
Loading
Loading
@@ -47,37 +55,32 @@ import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
*
* @param {Date} initialDate
*/
export const getTimeframeForQuartersView = (initialDate = new Date()) => {
const startDate = initialDate;
export const getTimeframeForQuartersView = (initialDate = new Date(), timeframe = []) => {
const startDate = newDate(initialDate);
startDate.setHours(0, 0, 0, 0);
 
const monthsForQuarters = {
1: [0, 1, 2],
2: [3, 4, 5],
3: [6, 7, 8],
4: [9, 10, 11],
};
// Get current quarter for current month
const currentQuarter = Math.floor((startDate.getMonth() + 3) / 3);
// Get index of current month in current quarter
// It could be 0, 1, 2 (i.e. first, second or third)
const currentMonthInCurrentQuarter = monthsForQuarters[currentQuarter].indexOf(
startDate.getMonth(),
);
if (!timeframe.length) {
// Get current quarter for current month
const currentQuarter = Math.floor((startDate.getMonth() + 3) / 3);
// Get index of current month in current quarter
// It could be 0, 1, 2 (i.e. first, second or third)
const currentMonthInCurrentQuarter = monthsForQuarters[currentQuarter].indexOf(
startDate.getMonth(),
);
 
// To move start back to first month of previous quarter
// Adding quarter size (3) to month order will give us
// exact number of months we need to go back in time
const startMonth = currentMonthInCurrentQuarter + 3;
const quartersTimeframe = [];
// Move startDate to first month of previous quarter
startDate.setMonth(startDate.getMonth() - startMonth);
// To move start back to first month of 2 quarters prior by
// adding quarter size (3 + 3) to month order will give us
// exact number of months we need to go back in time
const startMonth = currentMonthInCurrentQuarter + 6;
// Move startDate to first month of previous quarter
startDate.setMonth(startDate.getMonth() - startMonth);
 
// Get timeframe for the length we determined for this preset
// start from the startDate
const timeframe = getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH);
// Get timeframe for the length we determined for this preset
// start from the startDate
timeframe.push(...getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH));
}
 
const quartersTimeframe = [];
// Iterate over the timeframe and break it down
// in chunks of quarters
for (let i = 0; i < timeframe.length; i += 3) {
Loading
Loading
@@ -100,83 +103,246 @@ export const getTimeframeForQuartersView = (initialDate = new Date()) => {
return quartersTimeframe;
};
 
export const extendTimeframeForQuartersView = (initialDate = new Date(), length) => {
const startDate = newDate(initialDate);
startDate.setDate(1);
startDate.setMonth(startDate.getMonth() + (length > 0 ? 1 : -1));
const timeframe = getTimeframeWindowFrom(startDate, length);
return getTimeframeForQuartersView(startDate, length > 0 ? timeframe : timeframe.reverse());
};
/**
* This method returns array of Dates respresenting Months based on provided initialDate
*
* For eg; If initialDate is 15th Jan 2018
* Then as per Roadmap specs, we need to show
* 1 month before current month AND
* 2 months before current month,
* current month AND
* 5 months after current month
* thus, total of 7 months.
* thus, total of 8 months.
*
* So returned array from this method will be;
* [
* 1 Dec 2017, 1 Jan 2018, 1 Feb 2018, 1 Mar 2018,
* 1 Apr 2018, 1 May 2018, 30 Jun 2018
* 1 Nov 2017, 1 Dec 2017, 1 Jan 2018, 1 Feb 2018,
* 1 Mar 2018, 1 Apr 2018, 1 May 2018, 30 Jun 2018
* ]
*
* @param {Date} initialDate
*/
export const getTimeframeForMonthsView = (initialDate = new Date()) => {
const startDate = initialDate;
startDate.setHours(0, 0, 0, 0);
const startDate = newDate(initialDate);
 
// Move startDate to a month prior to current month
startDate.setMonth(startDate.getMonth() - 1);
startDate.setMonth(startDate.getMonth() - 2);
 
return getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.MONTHS.TIMEFRAME_LENGTH);
};
 
export const extendTimeframeForMonthsView = (initialDate = new Date(), length) => {
const startDate = newDate(initialDate);
// When length is positive (which means extension is of type APPEND)
// Set initial date as first day of the month.
if (length > 0) {
startDate.setDate(1);
}
const timeframe = getTimeframeWindowFrom(startDate, length - 1).slice(1);
return length > 0 ? timeframe : timeframe.reverse();
};
/**
* This method returns array of Dates respresenting Months based on provided initialDate
*
* For eg; If initialDate is 15th Jan 2018
* Then as per Roadmap specs, we need to show
* 1 week before current week AND
* 2 weeks before current week,
* current week AND
* 4 weeks after current week
* thus, total of 6 weeks.
* thus, total of 7 weeks.
* Note that week starts on Sunday
*
* So returned array from this method will be;
* [
* 7 Jan 2018, 14 Jan 2018, 21 Jan 2018,
* 31 Dec 2017, 7 Jan 2018, 14 Jan 2018, 21 Jan 2018,
* 28 Jan 2018, 4 Mar 2018, 11 Mar 2018
* ]
*
* @param {Date} initialDate
*/
export const getTimeframeForWeeksView = (initialDate = new Date()) => {
const startDate = initialDate;
const startDate = newDate(initialDate);
startDate.setHours(0, 0, 0, 0);
 
const dayOfWeek = startDate.getDay();
const daysToFirstDayOfPrevWeek = dayOfWeek + 7;
const daysToFirstDayOfPrevWeek = dayOfWeek + 14;
const timeframe = [];
 
// Move startDate to first day (Sunday) of previous week
// Move startDate to first day (Sunday) of 2 weeks prior
startDate.setDate(startDate.getDate() - daysToFirstDayOfPrevWeek);
 
// Iterate for the length of this preset
for (let i = 0; i < PRESET_DEFAULTS.WEEKS.TIMEFRAME_LENGTH; i += 1) {
// Push date to timeframe only when day is
// first day (Sunday) of the week1
if (startDate.getDay() === 0) {
timeframe.push(new Date(startDate.getTime()));
}
// Move date one day further
startDate.setDate(startDate.getDate() + 1);
// first day (Sunday) of the week
timeframe.push(newDate(startDate));
// Move date next Sunday
startDate.setDate(startDate.getDate() + 7);
}
 
return timeframe;
};
 
export const getTimeframeForPreset = (presetType = PRESET_TYPES.MONTHS) => {
export const extendTimeframeForWeeksView = (initialDate = new Date(), length) => {
const startDate = newDate(initialDate);
if (length < 0) {
startDate.setDate(startDate.getDate() + (length + 1) * 7);
} else {
startDate.setDate(startDate.getDate() + 7);
}
const timeframe = getTimeframeForWeeksView(startDate, length + 1);
return timeframe.slice(1);
};
export const extendTimeframeForPreset = ({
presetType = PRESET_TYPES.MONTHS,
extendAs = EXTEND_AS.PREPEND,
extendByLength = 0,
initialDate,
}) => {
if (presetType === PRESET_TYPES.QUARTERS) {
return getTimeframeForQuartersView();
const length = extendByLength || PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH;
return extendTimeframeForQuartersView(
initialDate,
extendAs === EXTEND_AS.PREPEND ? -length : length,
);
} else if (presetType === PRESET_TYPES.MONTHS) {
return getTimeframeForMonthsView();
const length = extendByLength || PRESET_DEFAULTS.MONTHS.TIMEFRAME_LENGTH;
return extendTimeframeForMonthsView(
initialDate,
extendAs === EXTEND_AS.PREPEND ? -length : length,
);
}
return getTimeframeForWeeksView();
const length = extendByLength || PRESET_DEFAULTS.WEEKS.TIMEFRAME_LENGTH;
return extendTimeframeForWeeksView(
initialDate,
extendAs === EXTEND_AS.PREPEND ? -length : length,
);
};
export const extendTimeframeForAvailableWidth = ({
timeframe,
timeframeStart,
timeframeEnd,
availableTimeframeWidth,
presetType,
}) => {
let timeframeLength = timeframe.length;
// Estimate how many more timeframe columns are needed
// to fill in extra screen space so that timeline becomes
// horizontally scrollable.
while (availableTimeframeWidth / timeframeLength > TIMELINE_CELL_MIN_WIDTH) {
timeframeLength += 1;
}
// We double the increaseLengthBy to make sure there's enough room
// to perform horizontal scroll without triggering timeframe extension
// on initial page load.
const increaseLengthBy = (timeframeLength - timeframe.length) * 2;
// If there are timeframe items to be added
// to make timeline scrollable, do as follows.
if (increaseLengthBy > 0) {
// Split length in 2 parts and get
// count for both prepend and append.
const prependBy = Math.floor(increaseLengthBy / 2);
const appendBy = Math.ceil(increaseLengthBy / 2);
if (prependBy) {
// Prepend the timeline with
// the count as given by prependBy
timeframe.unshift(
...extendTimeframeForPreset({
extendAs: EXTEND_AS.PREPEND,
initialDate: timeframeStart,
// In case of presetType `quarters`, length would represent
// number of months for total quarters, hence we do `* 3`.
extendByLength: presetType === PRESET_TYPES.QUARTERS ? prependBy * 3 : prependBy,
presetType,
}),
);
}
if (appendBy) {
// Append the timeline with
// the count as given by appendBy
timeframe.push(
...extendTimeframeForPreset({
extendAs: EXTEND_AS.APPEND,
initialDate: timeframeEnd,
// In case of presetType `quarters`, length would represent
// number of months for total quarters, hence we do `* 3`.
//
// For other preset types, we add `2` to appendBy to compensate for
// last item of original timeframe (month or week)
extendByLength: presetType === PRESET_TYPES.QUARTERS ? appendBy * 3 : appendBy + 2,
presetType,
}),
);
}
}
};
export const getTimeframeForPreset = (
presetType = PRESET_TYPES.MONTHS,
availableTimeframeWidth = 0,
) => {
let timeframe;
let timeframeStart;
let timeframeEnd;
// Get timeframe based on presetType and
// extract timeframeStart and timeframeEnd
// date objects
if (presetType === PRESET_TYPES.QUARTERS) {
timeframe = getTimeframeForQuartersView();
[timeframeStart] = timeframe[0].range;
// eslint-disable-next-line prefer-destructuring
timeframeEnd = timeframe[timeframe.length - 1].range[2];
} else if (presetType === PRESET_TYPES.MONTHS) {
timeframe = getTimeframeForMonthsView();
[timeframeStart] = timeframe;
timeframeEnd = timeframe[timeframe.length - 1];
} else {
timeframe = getTimeframeForWeeksView();
timeframeStart = newDate(timeframe[0]);
timeframeEnd = newDate(timeframe[timeframe.length - 1]);
timeframeStart.setDate(timeframeStart.getDate() - 7); // Move date back by a week
timeframeEnd.setDate(timeframeEnd.getDate() + 7); // Move date ahead by a week
}
// Extend timeframe on initial load to ensure
// timeline is horizontally scrollable in all
// screen sizes.
extendTimeframeForAvailableWidth({
timeframe,
timeframeStart,
timeframeEnd,
availableTimeframeWidth,
presetType,
});
return timeframe;
};
 
export const getEpicsPathForPreset = ({
Loading
Loading
@@ -184,7 +350,7 @@ export const getEpicsPathForPreset = ({
filterQueryString = '',
presetType = '',
timeframe = [],
state = 'all',
epicsState = 'all',
}) => {
let start;
let end;
Loading
Loading
@@ -208,13 +374,13 @@ export const getEpicsPathForPreset = ({
end = lastTimeframe;
} else if (presetType === PRESET_TYPES.WEEKS) {
start = firstTimeframe;
end = new Date(lastTimeframe.getTime());
end = newDate(lastTimeframe);
end.setDate(end.getDate() + 6);
}
 
const startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
const endDate = `${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`;
epicsPath += `?state=${state}&start_date=${startDate}&end_date=${endDate}`;
epicsPath += `?state=${epicsState}&start_date=${startDate}&end_date=${endDate}`;
 
if (filterQueryString) {
epicsPath += `&${filterQueryString}`;
Loading
Loading
@@ -222,3 +388,43 @@ export const getEpicsPathForPreset = ({
 
return epicsPath;
};
export const sortEpics = (epics, sortedBy) => {
const sortByStartDate = sortedBy.indexOf('start_date') > -1;
const sortOrderAsc = sortedBy.indexOf('asc') > -1;
epics.sort((a, b) => {
let aDate;
let bDate;
if (sortByStartDate) {
aDate = a.startDate;
if (a.startDateOutOfRange) {
aDate = a.originalStartDate;
}
bDate = b.startDate;
if (b.startDateOutOfRange) {
bDate = b.originalStartDate;
}
} else {
aDate = a.endDate;
if (a.endDateOutOfRange) {
aDate = a.originalEndDate;
}
bDate = b.endDate;
if (b.endDateOutOfRange) {
bDate = b.originalEndDate;
}
}
// Sort in ascending or descending order
if (aDate.getTime() < bDate.getTime()) {
return sortOrderAsc ? -1 : 1;
} else if (aDate.getTime() > bDate.getTime()) {
return sortOrderAsc ? 1 : -1;
}
return 0;
});
};
Loading
Loading
@@ -20,6 +20,36 @@ $column-right-gradient: linear-gradient(
$roadmap-gradient-gray 100%
);
 
@keyframes colorTransitionDetailsCell {
from {
background-color: $blue-100;
}
to {
background-color: $white-light;
}
}
@keyframes fadeInDetails {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeinTimelineBar {
from {
opacity: 0;
}
to {
opacity: 0.75;
}
}
@mixin roadmap-scroll-mixin {
height: $grid-size;
width: $details-cell-width;
Loading
Loading
@@ -229,6 +259,12 @@ $column-right-gradient: linear-gradient(
}
}
 
&.newly-added-epic {
.epic-details-cell {
animation: colorTransitionDetailsCell 3s;
}
}
.epic-details-cell,
.epic-timeline-cell {
box-sizing: border-box;
Loading
Loading
@@ -251,6 +287,11 @@ $column-right-gradient: linear-gradient(
height: $item-height;
}
 
.epic-title,
.epic-group-timeframe {
animation: fadeInDetails 1s;
}
.epic-title {
display: table;
table-layout: fixed;
Loading
Loading
@@ -294,25 +335,12 @@ $column-right-gradient: linear-gradient(
background-color: $blue-500;
border-radius: $border-radius-default;
opacity: 0.75;
animation: fadeinTimelineBar 1s;
 
&:hover {
opacity: 1;
}
 
&.start-date-outside::before,
&.end-date-outside::after {
content: "";
position: absolute;
top: 0;
height: 100%;
}
&.start-date-outside::before,
&.end-date-outside::after {
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
}
&.start-date-undefined {
background: linear-gradient(
to right,
Loading
Loading
@@ -330,31 +358,6 @@ $column-right-gradient: linear-gradient(
$roadmap-gradient-gray 100%
);
}
&.start-date-outside {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
&::before {
left: -$grid-size;
border-right: $grid-size solid $blue-500;
}
}
&.end-date-outside {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
&::after {
right: -$grid-size;
border-left: $grid-size solid $blue-500;
}
}
&.start-date-outside,
&.start-date-undefined.end-date-outside {
left: $grid-size;
}
}
 
&:last-child {
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment