Commit 4bc1239b authored by Phil Hughes's avatar Phil Hughes

Merge branch 'kp-improve-roadmap-today-indicator-render-logic' into 'master'

Improve Roadmap today indicator rendering logic

See merge request gitlab-org/gitlab!24669
parents c5a8c8a9 ef59084d
<script>
import CommonMixin from '../mixins/common_mixin';
export default {
mixins: [CommonMixin],
props: {
presetType: {
type: String,
required: true,
},
timeframeItem: {
type: [Date, Object],
required: true,
},
},
data() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
return {
currentDate,
indicatorStyles: {},
};
},
mounted() {
this.$nextTick(() => {
this.indicatorStyles = this.getIndicatorStyles();
});
},
};
</script>
<template>
<span
v-if="hasToday"
:style="indicatorStyles"
class="current-day-indicator position-absolute"
></span>
</template>
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import CommonMixin from '../mixins/common_mixin';
import QuartersPresetMixin from '../mixins/quarters_preset_mixin'; import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
import MonthsPresetMixin from '../mixins/months_preset_mixin'; import MonthsPresetMixin from '../mixins/months_preset_mixin';
import WeeksPresetMixin from '../mixins/weeks_preset_mixin'; import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants'; import CurrentDayIndicator from './current_day_indicator.vue';
import { TIMELINE_CELL_MIN_WIDTH } from '../constants';
export default { export default {
cellWidth: TIMELINE_CELL_MIN_WIDTH, cellWidth: TIMELINE_CELL_MIN_WIDTH,
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [QuartersPresetMixin, MonthsPresetMixin, WeeksPresetMixin], components: {
CurrentDayIndicator,
},
mixins: [CommonMixin, QuartersPresetMixin, MonthsPresetMixin, WeeksPresetMixin],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -55,11 +61,11 @@ export default { ...@@ -55,11 +61,11 @@ export default {
}; };
}, },
hasStartDate() { hasStartDate() {
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetTypeQuarters) {
return this.hasStartDateForQuarter(); return this.hasStartDateForQuarter();
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetTypeMonths) {
return this.hasStartDateForMonth(); return this.hasStartDateForMonth();
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetTypeWeeks) {
return this.hasStartDateForWeek(); return this.hasStartDateForWeek();
} }
return false; return false;
...@@ -68,14 +74,14 @@ export default { ...@@ -68,14 +74,14 @@ export default {
let barStyles = {}; let barStyles = {};
if (this.hasStartDate) { if (this.hasStartDate) {
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetTypeQuarters) {
// CSS properties are a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/24 // CSS properties are a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/24
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
barStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`; barStyles = `width: ${this.getTimelineBarWidthForQuarters()}px; ${this.getTimelineBarStartOffsetForQuarters()}`;
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetTypeMonths) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
barStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`; barStyles = `width: ${this.getTimelineBarWidthForMonths()}px; ${this.getTimelineBarStartOffsetForMonths()}`;
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetTypeWeeks) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
barStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`; barStyles = `width: ${this.getTimelineBarWidthForWeeks()}px; ${this.getTimelineBarStartOffsetForWeeks()}`;
} }
...@@ -88,6 +94,7 @@ export default { ...@@ -88,6 +94,7 @@ export default {
<template> <template>
<span class="epic-timeline-cell" data-qa-selector="epic_timeline_cell"> <span class="epic-timeline-cell" data-qa-selector="epic_timeline_cell">
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<div class="timeline-bar-wrapper"> <div class="timeline-bar-wrapper">
<a <a
v-if="hasStartDate" v-if="hasStartDate"
......
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility'; import { dateInWords } from '~/lib/utils/datetime_utility';
import { PRESET_TYPES, emptyStateDefault, emptyStateWithFilters } from '../constants'; import CommonMixin from '../mixins/common_mixin';
import { emptyStateDefault, emptyStateWithFilters } from '../constants';
import initEpicCreate from '../../epic/epic_bundle'; import initEpicCreate from '../../epic/epic_bundle';
export default { export default {
mixins: [CommonMixin],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -43,7 +45,7 @@ export default { ...@@ -43,7 +45,7 @@ export default {
let startDate; let startDate;
let endDate; let endDate;
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetTypeQuarters) {
const quarterStart = this.timeframeStart.range[0]; const quarterStart = this.timeframeStart.range[0];
const quarterEnd = this.timeframeEnd.range[2]; const quarterEnd = this.timeframeEnd.range[2];
startDate = dateInWords( startDate = dateInWords(
...@@ -52,14 +54,14 @@ export default { ...@@ -52,14 +54,14 @@ export default {
quarterStart.getFullYear() === quarterEnd.getFullYear(), quarterStart.getFullYear() === quarterEnd.getFullYear(),
); );
endDate = dateInWords(quarterEnd, true); endDate = dateInWords(quarterEnd, true);
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetTypeMonths) {
startDate = dateInWords( startDate = dateInWords(
this.timeframeStart, this.timeframeStart,
true, true,
this.timeframeStart.getFullYear() === this.timeframeEnd.getFullYear(), this.timeframeStart.getFullYear() === this.timeframeEnd.getFullYear(),
); );
endDate = dateInWords(this.timeframeEnd, true); endDate = dateInWords(this.timeframeEnd, true);
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetTypeWeeks) {
const end = new Date(this.timeframeEnd.getTime()); const end = new Date(this.timeframeEnd.getTime());
end.setDate(end.getDate() + 6); end.setDate(end.getDate() + 6);
......
...@@ -9,6 +9,7 @@ import eventHub from '../event_hub'; ...@@ -9,6 +9,7 @@ import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
import EpicItem from './epic_item.vue'; import EpicItem from './epic_item.vue';
import CurrentDayIndicator from './current_day_indicator.vue';
export default { export default {
EpicItem, EpicItem,
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
components: { components: {
VirtualList, VirtualList,
EpicItem, EpicItem,
CurrentDayIndicator,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
...@@ -143,6 +145,7 @@ export default { ...@@ -143,6 +145,7 @@ export default {
v-for="(epic, index) in epics" v-for="(epic, index) in epics"
ref="epicItems" ref="epicItems"
:key="index" :key="index"
:first-epic="index === 0"
:preset-type="presetType" :preset-type="presetType"
:epic="epic" :epic="epic"
:timeframe="timeframe" :timeframe="timeframe"
...@@ -155,11 +158,9 @@ export default { ...@@ -155,11 +158,9 @@ export default {
class="epics-list-item epics-list-item-empty clearfix" class="epics-list-item epics-list-item-empty clearfix"
> >
<span class="epic-details-cell"></span> <span class="epic-details-cell"></span>
<span <span v-for="(timeframeItem, index) in timeframe" :key="index" class="epic-timeline-cell">
v-for="(timeframeItem, index) in timeframe" <current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
:key="index" </span>
class="epic-timeline-cell"
></span>
</div> </div>
<div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div> <div v-show="showBottomShadow" :style="shadowCellStyles" class="scroll-bottom-shadow"></div>
</div> </div>
......
<script> <script>
import { getSundays } from '~/lib/utils/datetime_utility'; import { getSundays } from '~/lib/utils/datetime_utility';
import { PRESET_TYPES } from '../../constants'; import CommonMixin from '../../mixins/common_mixin';
import timelineTodayIndicator from '../timeline_today_indicator.vue'; import { PRESET_TYPES } from '../../constants';
export default { export default {
presetType: PRESET_TYPES.MONTHS, mixins: [CommonMixin],
components: {
timelineTodayIndicator,
},
props: { props: {
currentDate: { currentDate: {
type: Date, type: Date,
...@@ -20,6 +17,12 @@ export default { ...@@ -20,6 +17,12 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
presetType: PRESET_TYPES.MONTHS,
indicatorStyle: {},
};
},
computed: { computed: {
headerSubItems() { headerSubItems() {
return getSundays(this.timeframeItem); return getSundays(this.timeframeItem);
...@@ -33,15 +36,11 @@ export default { ...@@ -33,15 +36,11 @@ export default {
// Show dark color text only for dates from current month and future months. // Show dark color text only for dates from current month and future months.
return timeframeYear >= currentYear && timeframeMonth >= currentMonth ? 'label-dark' : ''; return timeframeYear >= currentYear && timeframeMonth >= currentMonth ? 'label-dark' : '';
}, },
hasToday() { },
const timeframeYear = this.timeframeItem.getFullYear(); mounted() {
const timeframeMonth = this.timeframeItem.getMonth(); this.$nextTick(() => {
this.indicatorStyle = this.getIndicatorStyles();
return ( });
this.currentDate.getMonth() === timeframeMonth &&
this.currentDate.getFullYear() === timeframeYear
);
},
}, },
methods: { methods: {
getSubItemValueClass(subItem) { getSubItemValueClass(subItem) {
...@@ -71,14 +70,12 @@ export default { ...@@ -71,14 +70,12 @@ export default {
:key="index" :key="index"
:class="getSubItemValueClass(subItem)" :class="getSubItemValueClass(subItem)"
class="sublabel-value" class="sublabel-value"
>{{ subItem.getDate() }}</span
> >
{{ subItem.getDate() }} <span
</span>
<timeline-today-indicator
v-if="hasToday" v-if="hasToday"
:preset-type="$options.presetType" :style="indicatorStyle"
:current-date="currentDate" class="current-day-indicator-header preset-months position-absolute"
:timeframe-item="timeframeItem" ></span>
/>
</div> </div>
</template> </template>
<script> <script>
import { monthInWords } from '~/lib/utils/datetime_utility'; import { monthInWords } from '~/lib/utils/datetime_utility';
import { PRESET_TYPES } from '../../constants'; import CommonMixin from '../../mixins/common_mixin';
import timelineTodayIndicator from '../timeline_today_indicator.vue'; import { PRESET_TYPES } from '../../constants';
export default { export default {
presetType: PRESET_TYPES.QUARTERS, mixins: [CommonMixin],
components: {
timelineTodayIndicator,
},
props: { props: {
currentDate: { currentDate: {
type: Date, type: Date,
...@@ -20,6 +17,12 @@ export default { ...@@ -20,6 +17,12 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
presetType: PRESET_TYPES.QUARTERS,
indicatorStyle: {},
};
},
computed: { computed: {
quarterBeginDate() { quarterBeginDate() {
return this.timeframeItem.range[0]; return this.timeframeItem.range[0];
...@@ -30,9 +33,11 @@ export default { ...@@ -30,9 +33,11 @@ export default {
headerSubItems() { headerSubItems() {
return this.timeframeItem.range; return this.timeframeItem.range;
}, },
hasToday() { },
return this.currentDate >= this.quarterBeginDate && this.currentDate <= this.quarterEndDate; mounted() {
}, this.$nextTick(() => {
this.indicatorStyle = this.getIndicatorStyles();
});
}, },
methods: { methods: {
getSubItemValueClass(subItem) { getSubItemValueClass(subItem) {
...@@ -62,14 +67,12 @@ export default { ...@@ -62,14 +67,12 @@ export default {
:key="index" :key="index"
:class="getSubItemValueClass(subItem)" :class="getSubItemValueClass(subItem)"
class="sublabel-value" class="sublabel-value"
>{{ getSubItemValue(subItem) }}</span
> >
{{ getSubItemValue(subItem) }} <span
</span>
<timeline-today-indicator
v-if="hasToday" v-if="hasToday"
:preset-type="$options.presetType" :style="indicatorStyle"
:current-date="currentDate" class="current-day-indicator-header preset-quarters position-absolute"
:timeframe-item="timeframeItem" ></span>
/>
</div> </div>
</template> </template>
<script> <script>
import { PRESET_TYPES } from '../../constants'; import CommonMixin from '../../mixins/common_mixin';
import timelineTodayIndicator from '../timeline_today_indicator.vue'; import { PRESET_TYPES } from '../../constants';
export default { export default {
presetType: PRESET_TYPES.WEEKS, mixins: [CommonMixin],
components: {
timelineTodayIndicator,
},
props: { props: {
currentDate: { currentDate: {
type: Date, type: Date,
...@@ -18,6 +15,12 @@ export default { ...@@ -18,6 +15,12 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
presetType: PRESET_TYPES.WEEKS,
indicatorStyle: {},
};
},
computed: { computed: {
headerSubItems() { headerSubItems() {
const timeframeItem = new Date(this.timeframeItem.getTime()); const timeframeItem = new Date(this.timeframeItem.getTime());
...@@ -34,12 +37,11 @@ export default { ...@@ -34,12 +37,11 @@ export default {
return headerSubItems; return headerSubItems;
}, },
hasToday() { },
return ( mounted() {
this.currentDate.getTime() >= this.headerSubItems[0].getTime() && this.$nextTick(() => {
this.currentDate.getTime() <= this.headerSubItems[this.headerSubItems.length - 1].getTime() this.indicatorStyle = this.getIndicatorStyles();
); });
},
}, },
methods: { methods: {
getSubItemValueClass(subItem) { getSubItemValueClass(subItem) {
...@@ -62,14 +64,12 @@ export default { ...@@ -62,14 +64,12 @@ export default {
:key="index" :key="index"
:class="getSubItemValueClass(subItem)" :class="getSubItemValueClass(subItem)"
class="sublabel-value" class="sublabel-value"
>{{ subItem.getDate() }}</span
> >
{{ subItem.getDate() }} <span
</span>
<timeline-today-indicator
v-if="hasToday" v-if="hasToday"
:preset-type="$options.presetType" :style="indicatorStyle"
:current-date="currentDate" class="current-day-indicator-header preset-weeks position-absolute"
:timeframe-item="timeframeItem" ></span>
/>
</div> </div>
</template> </template>
<script> <script>
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants'; import CommonMixin from '../mixins/common_mixin';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH } from '../constants';
import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue'; import QuartersHeaderItem from './preset_quarters/quarters_header_item.vue';
import MonthsHeaderItem from './preset_months/months_header_item.vue'; import MonthsHeaderItem from './preset_months/months_header_item.vue';
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
MonthsHeaderItem, MonthsHeaderItem,
WeeksHeaderItem, WeeksHeaderItem,
}, },
mixins: [CommonMixin],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -34,11 +36,11 @@ export default { ...@@ -34,11 +36,11 @@ export default {
}, },
computed: { computed: {
headerItemComponentForPreset() { headerItemComponentForPreset() {
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetTypeQuarters) {
return 'quarters-header-item'; return 'quarters-header-item';
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetTypeMonths) {
return 'months-header-item'; return 'months-header-item';
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetTypeWeeks) {
return 'weeks-header-item'; return 'weeks-header-item';
} }
return ''; return '';
......
<script>
import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility'; import { totalDaysInMonth, dayInQuarter, totalDaysInQuarter } from '~/lib/utils/datetime_utility';
import { EPIC_DETAILS_CELL_WIDTH, PRESET_TYPES, DAYS_IN_WEEK, SCROLL_BAR_SIZE } from '../constants'; import { PRESET_TYPES, DAYS_IN_WEEK } from '../constants';
import eventHub from '../event_hub';
export default { export default {
props: { computed: {
presetType: { presetTypeQuarters() {
type: String, return this.presetType === PRESET_TYPES.QUARTERS;
required: true,
}, },
currentDate: { presetTypeMonths() {
type: Date, return this.presetType === PRESET_TYPES.MONTHS;
required: true,
}, },
timeframeItem: { presetTypeWeeks() {
type: [Date, Object], return this.presetType === PRESET_TYPES.WEEKS;
required: true, },
hasToday() {
if (this.presetTypeQuarters) {
return (
this.currentDate >= this.timeframeItem.range[0] &&
this.currentDate <= this.timeframeItem.range[2]
);
} else if (this.presetTypeMonths) {
return (
this.currentDate.getMonth() === this.timeframeItem.getMonth() &&
this.currentDate.getFullYear() === this.timeframeItem.getFullYear()
);
}
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 (
this.currentDate.getTime() >= headerSubItems[0].getTime() &&
this.currentDate.getTime() <= headerSubItems[headerSubItems.length - 1].getTime()
);
}, },
},
data() {
return {
todayBarStyles: {},
todayBarReady: true,
};
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
this.$nextTick(() => {
this.todayBarStyles = this.getTodayBarStyles();
});
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
}, },
methods: { methods: {
getTodayBarStyles() { getIndicatorStyles() {
let left; let left;
// 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
// based on the current presetType // based on the current presetType
if (this.presetType === PRESET_TYPES.QUARTERS) { if (this.presetTypeQuarters) {
left = Math.floor( left = Math.floor(
(dayInQuarter(this.currentDate, this.timeframeItem.range) / (dayInQuarter(this.currentDate, this.timeframeItem.range) /
totalDaysInQuarter(this.timeframeItem.range)) * totalDaysInQuarter(this.timeframeItem.range)) *
100, 100,
); );
} else if (this.presetType === PRESET_TYPES.MONTHS) { } else if (this.presetTypeMonths) {
left = Math.floor( left = Math.floor(
(this.currentDate.getDate() / totalDaysInMonth(this.timeframeItem)) * 100, (this.currentDate.getDate() / totalDaysInMonth(this.timeframeItem)) * 100,
); );
} else if (this.presetType === PRESET_TYPES.WEEKS) { } else if (this.presetTypeWeeks) {
left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK); left = Math.floor(((this.currentDate.getDay() + 1) / DAYS_IN_WEEK) * 100 - DAYS_IN_WEEK);
} }
return { return {
left: `${left}%`, left: `${left}%`,
height: `calc(100vh - ${this.$el.getBoundingClientRect().y + SCROLL_BAR_SIZE}px)`,
}; };
}, },
handleEpicsListScroll() {
const indicatorX = this.$el.getBoundingClientRect().x;
const rootOffsetLeft = this.$root.$el.parentElement.offsetLeft;
// 3px to compensate size of bubble on top of Indicator
this.todayBarReady = indicatorX - rootOffsetLeft >= EPIC_DETAILS_CELL_WIDTH + 3;
},
}, },
}; };
</script>
<template>
<span :class="{ invisible: !todayBarReady }" :style="todayBarStyles" class="today-bar"></span>
</template>
...@@ -161,7 +161,7 @@ html.group-epics-roadmap-html { ...@@ -161,7 +161,7 @@ html.group-epics-roadmap-html {
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: 0; top: 0;
z-index: 3; z-index: 20;
.timeline-header-blank, .timeline-header-blank,
.timeline-header-item { .timeline-header-item {
...@@ -224,25 +224,15 @@ html.group-epics-roadmap-html { ...@@ -224,25 +224,15 @@ html.group-epics-roadmap-html {
line-height: 1.5; line-height: 1.5;
padding: 2px 0; padding: 2px 0;
} }
}
.today-bar { .current-day-indicator-header {
position: absolute; bottom: 0;
top: 20px; height: $gl-vert-padding;
width: 2px; width: $gl-vert-padding;
background-color: $red-500; background-color: $red-500;
pointer-events: none; border-radius: 50%;
} transform: translateX(-2px);
.today-bar::before {
content: '';
position: absolute;
top: -2px;
left: -3px;
height: $grid-size;
width: $grid-size;
background-color: inherit;
border-radius: 50%;
}
} }
} }
...@@ -275,7 +265,6 @@ html.group-epics-roadmap-html { ...@@ -275,7 +265,6 @@ html.group-epics-roadmap-html {
.epic-details-cell, .epic-details-cell,
.epic-timeline-cell { .epic-timeline-cell {
background-color: $white-light; background-color: $white-light;
border-bottom: 0;
} }
} }
...@@ -308,7 +297,7 @@ html.group-epics-roadmap-html { ...@@ -308,7 +297,7 @@ html.group-epics-roadmap-html {
padding: $gl-padding-8 $gl-padding; padding: $gl-padding-8 $gl-padding;
font-size: $code-font-size; font-size: $code-font-size;
background-color: $white-light; background-color: $white-light;
z-index: 2; z-index: 10;
&::after { &::after {
height: $item-height; height: $item-height;
...@@ -348,10 +337,19 @@ html.group-epics-roadmap-html { ...@@ -348,10 +337,19 @@ html.group-epics-roadmap-html {
} }
.epic-timeline-cell { .epic-timeline-cell {
position: relative;
width: $timeline-cell-width; width: $timeline-cell-width;
background-color: transparent; background-color: transparent;
border-right: $border-style; border-right: $border-style;
.current-day-indicator {
top: -1px;
width: 2px;
height: calc(100% + 1px);
background-color: $red-500;
pointer-events: none;
}
.timeline-bar-wrapper { .timeline-bar-wrapper {
position: relative; position: relative;
} }
...@@ -364,6 +362,7 @@ html.group-epics-roadmap-html { ...@@ -364,6 +362,7 @@ html.group-epics-roadmap-html {
border-radius: $border-radius-default; border-radius: $border-radius-default;
opacity: 0.75; opacity: 0.75;
will-change: width, left; will-change: width, left;
z-index: 5;
&:hover { &:hover {
opacity: 1; opacity: 1;
......
import { shallowMount } from '@vue/test-utils';
import CurrentDayIndicator from 'ee/roadmap/components/current_day_indicator.vue';
import {
getTimeframeForQuartersView,
getTimeframeForMonthsView,
getTimeframeForWeeksView,
} from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import { mockTimeframeInitialDate } from '../mock_data';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const createComponent = () =>
shallowMount(CurrentDayIndicator, {
propsData: {
presetType: PRESET_TYPES.MONTHS,
timeframeItem: mockTimeframeMonths[0],
},
});
describe('CurrentDayIndicator', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('data', () => {
it('initializes currentDate and indicatorStyles props with default values', () => {
const currentDate = new Date();
expect(wrapper.vm.currentDate.getDate()).toBe(currentDate.getDate());
expect(wrapper.vm.currentDate.getMonth()).toBe(currentDate.getMonth());
expect(wrapper.vm.currentDate.getFullYear()).toBe(currentDate.getFullYear());
expect(wrapper.vm.indicatorStyles).toBeDefined();
});
});
describe('computed', () => {
describe('hasToday', () => {
it('returns true when presetType is QUARTERS and currentDate is within current quarter', done => {
wrapper.setData({
currentDate: mockTimeframeQuarters[0].range[1],
});
wrapper.setProps({
presetType: PRESET_TYPES.QUARTERS,
timeframeItem: mockTimeframeQuarters[0],
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.hasToday).toBe(true);
done();
});
});
it('returns true when presetType is MONTHS and currentDate is within current month', done => {
wrapper.setData({
currentDate: new Date(2020, 0, 15),
});
wrapper.setProps({
presetType: PRESET_TYPES.MONTHS,
timeframeItem: new Date(2020, 0, 1),
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.hasToday).toBe(true);
done();
});
});
it('returns true when presetType is WEEKS and currentDate is within current week', done => {
wrapper.setData({
currentDate: mockTimeframeWeeks[0],
});
wrapper.setProps({
presetType: PRESET_TYPES.WEEKS,
timeframeItem: mockTimeframeWeeks[0],
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.hasToday).toBe(true);
done();
});
});
});
});
describe('methods', () => {
describe('getIndicatorStyles', () => {
it('returns object containing `left` with value `34%` when presetType is QUARTERS', done => {
wrapper.setData({
currentDate: mockTimeframeQuarters[0].range[1],
});
wrapper.setProps({
presetType: PRESET_TYPES.QUARTERS,
timeframeItem: mockTimeframeQuarters[0],
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getIndicatorStyles()).toEqual(
jasmine.objectContaining({
left: '34%',
}),
);
done();
});
});
it('returns object containing `left` with value `48%` when presetType is MONTHS', done => {
wrapper.setData({
currentDate: new Date(2020, 0, 15),
});
wrapper.setProps({
presetType: PRESET_TYPES.MONTHS,
timeframeItem: new Date(2020, 0, 1),
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getIndicatorStyles()).toEqual(
jasmine.objectContaining({
left: '48%',
}),
);
done();
});
});
it('returns object containing `left` with value `7%` when presetType is WEEKS', done => {
wrapper.setData({
currentDate: mockTimeframeWeeks[0],
});
wrapper.setProps({
presetType: PRESET_TYPES.WEEKS,
timeframeItem: mockTimeframeWeeks[0],
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getIndicatorStyles()).toEqual(
jasmine.objectContaining({
left: '7%',
}),
);
done();
});
});
});
});
describe('template', () => {
beforeEach(done => {
wrapper.setData({
currentDate: mockTimeframeMonths[0],
});
wrapper.vm.$nextTick(() => {
done();
});
});
it('renders span element containing class `current-day-indicator`', () => {
expect(wrapper.element.classList.contains('current-day-indicator')).toBe(true);
});
it('renders span element with style attribute containing `left: 3%;`', () => {
expect(wrapper.element.getAttribute('style')).toBe('left: 3%;');
});
});
});
...@@ -74,6 +74,15 @@ describe('EpicItemTimelineComponent', () => { ...@@ -74,6 +74,15 @@ describe('EpicItemTimelineComponent', () => {
expect(vm.$el.classList.contains('epic-timeline-cell')).toBe(true); expect(vm.$el.classList.contains('epic-timeline-cell')).toBe(true);
}); });
it('renders current day indicator element', () => {
const currentDate = new Date();
vm = createComponent({
timeframeItem: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
});
expect(vm.$el.querySelector('span.current-day-indicator')).not.toBeNull();
});
it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => { it('renders timeline bar element with class `timeline-bar` and class `timeline-bar-wrapper` as container element', () => {
vm = createComponent({ vm = createComponent({
epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }), epic: Object.assign({}, mockEpic, { startDate: mockTimeframeMonths[1] }),
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import MonthsHeaderSubItemComponent from 'ee/roadmap/components/preset_months/months_header_sub_item.vue'; import MonthsHeaderSubItemComponent from 'ee/roadmap/components/preset_months/months_header_sub_item.vue';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
...@@ -27,6 +28,15 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -27,6 +28,15 @@ describe('MonthsHeaderSubItemComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('data', () => {
it('initializes `presetType` and `indicatorStyles` data props', () => {
vm = createComponent({});
expect(vm.presetType).toBe(PRESET_TYPES.MONTHS);
expect(vm.indicatorStyle).toBeDefined();
});
});
describe('computed', () => { describe('computed', () => {
describe('headerSubItems', () => { describe('headerSubItems', () => {
it('returns array of dates containing Sundays from timeframeItem', () => { it('returns array of dates containing Sundays from timeframeItem', () => {
...@@ -55,23 +65,6 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -55,23 +65,6 @@ describe('MonthsHeaderSubItemComponent', () => {
expect(vm.headerSubItemClass).toBe(''); expect(vm.headerSubItemClass).toBe('');
}); });
}); });
describe('hasToday', () => {
it('returns true when current month and year is same as timeframe month and year', () => {
vm = createComponent({});
expect(vm.hasToday).toBe(true);
});
it('returns false when current month and year is different from timeframe month and year', () => {
vm = createComponent({
currentDate: new Date(2017, 10, 1), // Nov 1, 2017
timeframeItem: new Date(2018, 0, 1), // Jan 1, 2018
});
expect(vm.hasToday).toBe(false);
});
});
}); });
describe('methods', () => { describe('methods', () => {
...@@ -99,5 +92,9 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -99,5 +92,9 @@ describe('MonthsHeaderSubItemComponent', () => {
it('renders sub item element with class `sublabel-value`', () => { it('renders sub item element with class `sublabel-value`', () => {
expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull(); expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull();
}); });
it('renders element with class `current-day-indicator-header` when hasToday is true', () => {
expect(vm.$el.querySelector('.current-day-indicator-header.preset-months')).not.toBeNull();
});
}); });
}); });
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import QuartersHeaderSubItemComponent from 'ee/roadmap/components/preset_quarters/quarters_header_sub_item.vue'; import QuartersHeaderSubItemComponent from 'ee/roadmap/components/preset_quarters/quarters_header_sub_item.vue';
import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
...@@ -27,6 +28,15 @@ describe('QuartersHeaderSubItemComponent', () => { ...@@ -27,6 +28,15 @@ describe('QuartersHeaderSubItemComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('data', () => {
it('initializes `presetType` and `indicatorStyles` data props', () => {
vm = createComponent({});
expect(vm.presetType).toBe(PRESET_TYPES.QUARTERS);
expect(vm.indicatorStyle).toBeDefined();
});
});
describe('computed', () => { describe('computed', () => {
describe('quarterBeginDate', () => { describe('quarterBeginDate', () => {
it('returns first month from the `timeframeItem.range`', () => { it('returns first month from the `timeframeItem.range`', () => {
...@@ -54,23 +64,6 @@ describe('QuartersHeaderSubItemComponent', () => { ...@@ -54,23 +64,6 @@ describe('QuartersHeaderSubItemComponent', () => {
}); });
}); });
}); });
describe('hasToday', () => {
it('returns true when current quarter is same as timeframe quarter', () => {
vm = createComponent({});
expect(vm.hasToday).toBe(true);
});
it('returns false when current quarter month is different from timeframe quarter', () => {
vm = createComponent({
currentDate: new Date(2017, 10, 1), // Nov 1, 2017
timeframeItem: mockTimeframeQuarters[0], // 2018 Apr May Jun
});
expect(vm.hasToday).toBe(false);
});
});
}); });
describe('methods', () => { describe('methods', () => {
...@@ -98,5 +91,9 @@ describe('QuartersHeaderSubItemComponent', () => { ...@@ -98,5 +91,9 @@ describe('QuartersHeaderSubItemComponent', () => {
it('renders sub item element with class `sublabel-value`', () => { it('renders sub item element with class `sublabel-value`', () => {
expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull(); expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull();
}); });
it('renders element with class `current-day-indicator-header` when hasToday is true', () => {
expect(vm.$el.querySelector('.current-day-indicator-header.preset-quarters')).not.toBeNull();
});
}); });
}); });
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import WeeksHeaderSubItemComponent from 'ee/roadmap/components/preset_weeks/weeks_header_sub_item.vue'; import WeeksHeaderSubItemComponent from 'ee/roadmap/components/preset_weeks/weeks_header_sub_item.vue';
import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data'; import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
...@@ -27,6 +28,15 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -27,6 +28,15 @@ describe('MonthsHeaderSubItemComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('data', () => {
it('initializes `presetType` and `indicatorStyles` data props', () => {
vm = createComponent({});
expect(vm.presetType).toBe(PRESET_TYPES.WEEKS);
expect(vm.indicatorStyle).toBeDefined();
});
});
describe('computed', () => { describe('computed', () => {
describe('headerSubItems', () => { describe('headerSubItems', () => {
it('returns `headerSubItems` array of dates containing days of week from timeframeItem', () => { it('returns `headerSubItems` array of dates containing days of week from timeframeItem', () => {
...@@ -39,23 +49,6 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -39,23 +49,6 @@ describe('MonthsHeaderSubItemComponent', () => {
}); });
}); });
}); });
describe('hasToday', () => {
it('returns true when current week is same as timeframe week', () => {
vm = createComponent({});
expect(vm.hasToday).toBe(true);
});
it('returns false when current week is different from timeframe week', () => {
vm = createComponent({
currentDate: new Date(2017, 10, 1), // Nov 1, 2017
timeframeItem: new Date(2018, 0, 1), // Jan 1, 2018
});
expect(vm.hasToday).toBe(false);
});
});
}); });
describe('methods', () => { describe('methods', () => {
...@@ -93,5 +86,9 @@ describe('MonthsHeaderSubItemComponent', () => { ...@@ -93,5 +86,9 @@ describe('MonthsHeaderSubItemComponent', () => {
it('renders sub item element with class `sublabel-value`', () => { it('renders sub item element with class `sublabel-value`', () => {
expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull(); expect(vm.$el.querySelector('.sublabel-value')).not.toBeNull();
}); });
it('renders element with class `current-day-indicator-header` when hasToday is true', () => {
expect(vm.$el.querySelector('.current-day-indicator-header.preset-weeks')).not.toBeNull();
});
}); });
}); });
import Vue from 'vue';
import timelineTodayIndicatorComponent from 'ee/roadmap/components/timeline_today_indicator.vue';
import eventHub from 'ee/roadmap/event_hub';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTimeframeInitialDate } from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockCurrentDate = new Date(
mockTimeframeMonths[0].getFullYear(),
mockTimeframeMonths[0].getMonth(),
15,
);
const createComponent = ({
presetType = PRESET_TYPES.MONTHS,
currentDate = mockCurrentDate,
timeframeItem = mockTimeframeMonths[0],
}) => {
const Component = Vue.extend(timelineTodayIndicatorComponent);
return mountComponent(Component, {
presetType,
currentDate,
timeframeItem,
});
};
describe('TimelineTodayIndicatorComponent', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
vm = createComponent({});
expect(vm.todayBarStyles).toEqual({});
expect(vm.todayBarReady).toBe(true);
});
});
describe('methods', () => {
describe('getTodayBarStyles', () => {
it('sets `todayBarStyles` and `todayBarReady` props', () => {
vm = createComponent({});
const stylesObj = vm.getTodayBarStyles();
expect(stylesObj.height).toBe('calc(100vh - 16px)');
expect(stylesObj.left).toBe('50%');
});
});
});
describe('mounted', () => {
it('binds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$on');
const vmX = createComponent({});
expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds `epicsListScrolled` event listener via eventHub', () => {
spyOn(eventHub, '$off');
const vmX = createComponent({});
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
});
});
describe('template', () => {
it('renders component container element with class `today-bar`', done => {
vm = createComponent({});
vm.$nextTick(() => {
expect(vm.$el.classList.contains('today-bar')).toBe(true);
done();
});
});
});
});
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