Commit 84c13147 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '204994-roadmap-daterange-filter' into 'master'

Add daterange picker to filter Roadmap

See merge request gitlab-org/gitlab!55639
parents fc29b93e 48318f75
---
name: roadmap_daterange_filter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55639
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323917
milestone: '14.3'
type: development
group: group::product planning
default_enabled: false
...@@ -34,6 +34,6 @@ export default { ...@@ -34,6 +34,6 @@ export default {
<span <span
v-if="hasToday" v-if="hasToday"
:style="indicatorStyles" :style="indicatorStyles"
class="current-day-indicator position-absolute" class="current-day-indicator js-current-day-indicator gl-absolute"
></span> ></span>
</template> </template>
...@@ -5,7 +5,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -5,7 +5,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 eventHub from '../event_hub'; import eventHub from '../event_hub';
import { generateKey } from '../utils/epic_utils'; import { generateKey, scrollToCurrentDay } from '../utils/epic_utils';
import CurrentDayIndicator from './current_day_indicator.vue'; import CurrentDayIndicator from './current_day_indicator.vue';
import EpicItem from './epic_item.vue'; import EpicItem from './epic_item.vue';
...@@ -115,7 +115,7 @@ export default { ...@@ -115,7 +115,7 @@ export default {
// to timeline expand, so we wait for another render // to timeline expand, so we wait for another render
// cycle to complete. // cycle to complete.
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToTodayIndicator(); scrollToCurrentDay(this.$el);
}); });
if (!Object.keys(this.emptyRowContainerStyles).length) { if (!Object.keys(this.emptyRowContainerStyles).length) {
...@@ -139,13 +139,6 @@ export default { ...@@ -139,13 +139,6 @@ export default {
} }
return {}; return {};
}, },
/**
* Scroll timeframe to the right of the timeline
* by half the column size
*/
scrollToTodayIndicator() {
if (this.$el.parentElement) this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) { handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight; this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
}, },
......
...@@ -4,6 +4,7 @@ import { mapState, mapActions } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapState, mapActions } from 'vuex';
import { __, n__ } from '~/locale'; import { __, n__ } from '~/locale';
import { EPIC_DETAILS_CELL_WIDTH, EPIC_ITEM_HEIGHT, TIMELINE_CELL_MIN_WIDTH } from '../constants'; import { EPIC_DETAILS_CELL_WIDTH, EPIC_ITEM_HEIGHT, TIMELINE_CELL_MIN_WIDTH } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { scrollToCurrentDay } from '../utils/epic_utils';
import MilestoneTimeline from './milestone_timeline.vue'; import MilestoneTimeline from './milestone_timeline.vue';
const EXPAND_BUTTON_EXPANDED = { const EXPAND_BUTTON_EXPANDED = {
...@@ -97,13 +98,10 @@ export default { ...@@ -97,13 +98,10 @@ export default {
this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0; this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0;
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToTodayIndicator(); scrollToCurrentDay(this.$el);
}); });
}); });
}, },
scrollToTodayIndicator() {
if (this.$el.parentElement) this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) { handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight; this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
}, },
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
EXTEND_AS, EXTEND_AS,
EPICS_LIMIT_DISMISSED_COOKIE_NAME, EPICS_LIMIT_DISMISSED_COOKIE_NAME,
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT, EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT,
DATE_RANGES,
} from '../constants'; } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import EpicsListEmpty from './epics_list_empty.vue'; import EpicsListEmpty from './epics_list_empty.vue';
...@@ -32,6 +33,11 @@ export default { ...@@ -32,6 +33,11 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
timeframeRangeType: {
type: String,
required: false,
default: DATE_RANGES.CURRENT_QUARTER,
},
presetType: { presetType: {
type: String, type: String,
required: true, required: true,
...@@ -155,7 +161,7 @@ export default { ...@@ -155,7 +161,7 @@ export default {
<template> <template>
<div class="roadmap-app-container gl-h-full"> <div class="roadmap-app-container gl-h-full">
<roadmap-filters v-if="showFilteredSearchbar" /> <roadmap-filters v-if="showFilteredSearchbar" :timeframe-range-type="timeframeRangeType" />
<gl-alert <gl-alert
v-if="isWarningVisible" v-if="isWarningVisible"
variant="warning" variant="warning"
......
...@@ -9,18 +9,26 @@ import { ...@@ -9,18 +9,26 @@ import {
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EPICS_STATES, PRESET_TYPES } from '../constants'; import { EPICS_STATES, PRESET_TYPES, DATE_RANGES } from '../constants';
import EpicsFilteredSearchMixin from '../mixins/filtered_search_mixin'; import EpicsFilteredSearchMixin from '../mixins/filtered_search_mixin';
import { getPresetTypeForTimeframeRangeType } from '../utils/roadmap_utils';
const pickerType = {
Start: 'start',
End: 'end',
};
export default { export default {
pickerType,
epicStates: EPICS_STATES, epicStates: EPICS_STATES,
availablePresets: [ availableDateRanges: [
{ text: __('Quarters'), value: PRESET_TYPES.QUARTERS }, { text: s__('GroupRoadmap|This quarter'), value: DATE_RANGES.CURRENT_QUARTER },
{ text: __('Months'), value: PRESET_TYPES.MONTHS }, { text: s__('GroupRoadmap|This year'), value: DATE_RANGES.CURRENT_YEAR },
{ text: __('Weeks'), value: PRESET_TYPES.WEEKS }, { text: s__('GroupRoadmap|Within 3 years'), value: DATE_RANGES.THREE_YEARS },
], ],
availableSortOptions: [ availableSortOptions: [
{ {
...@@ -48,7 +56,18 @@ export default { ...@@ -48,7 +56,18 @@ export default {
GlDropdownDivider, GlDropdownDivider,
FilteredSearchBar, FilteredSearchBar,
}, },
mixins: [EpicsFilteredSearchMixin], mixins: [EpicsFilteredSearchMixin, glFeatureFlagsMixin()],
props: {
timeframeRangeType: {
type: String,
required: true,
},
},
data() {
return {
selectedDaterange: this.timeframeRangeType,
};
},
computed: { computed: {
...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams']), ...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams']),
selectedEpicStateTitle() { selectedEpicStateTitle() {
...@@ -59,6 +78,34 @@ export default { ...@@ -59,6 +78,34 @@ export default {
} }
return __('Closed epics'); return __('Closed epics');
}, },
daterangeDropdownText() {
switch (this.selectedDaterange) {
case DATE_RANGES.CURRENT_QUARTER:
return s__('GroupRoadmap|This quarter');
case DATE_RANGES.CURRENT_YEAR:
return s__('GroupRoadmap|This year');
case DATE_RANGES.THREE_YEARS:
return s__('GroupRoadmap|Within 3 years');
default:
return '';
}
},
availablePresets() {
const quarters = { text: __('Quarters'), value: PRESET_TYPES.QUARTERS };
const months = { text: __('Months'), value: PRESET_TYPES.MONTHS };
const weeks = { text: __('Weeks'), value: PRESET_TYPES.WEEKS };
if (!this.glFeatures.roadmapDaterangeFilter) {
return [quarters, months, weeks];
}
if (this.selectedDaterange === DATE_RANGES.CURRENT_YEAR) {
return [months, weeks];
} else if (this.selectedDaterange === DATE_RANGES.THREE_YEARS) {
return [quarters, months, weeks];
}
return [];
},
}, },
watch: { watch: {
urlParams: { urlParams: {
...@@ -77,8 +124,34 @@ export default { ...@@ -77,8 +124,34 @@ export default {
}, },
methods: { methods: {
...mapActions(['setEpicsState', 'setFilterParams', 'setSortedBy', 'fetchEpics']), ...mapActions(['setEpicsState', 'setFilterParams', 'setSortedBy', 'fetchEpics']),
handleDaterangeSelect(value) {
this.selectedDaterange = value;
},
handleDaterangeDropdownOpen() {
this.initialSelectedDaterange = this.selectedDaterange;
},
handleDaterangeDropdownClose() {
if (this.initialSelectedDaterange !== this.selectedDaterange) {
visitUrl(
mergeUrlParams(
{
timeframe_range_type: this.selectedDaterange,
layout: getPresetTypeForTimeframeRangeType(this.selectedDaterange),
},
window.location.href,
),
);
}
},
handleRoadmapLayoutChange(presetType) { handleRoadmapLayoutChange(presetType) {
visitUrl(mergeUrlParams({ layout: presetType }, window.location.href)); visitUrl(
mergeUrlParams(
this.glFeatures.roadmapDaterangeFilter
? { timeframe_range_type: this.selectedDaterange, layout: presetType }
: { layout: presetType },
window.location.href,
),
);
}, },
handleEpicStateChange(epicsState) { handleEpicStateChange(epicsState) {
this.setEpicsState(epicsState); this.setEpicsState(epicsState);
...@@ -99,12 +172,30 @@ export default { ...@@ -99,12 +172,30 @@ export default {
<template> <template>
<div class="epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui"> <div class="epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui">
<div <div
class="epics-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-xl-flex-direction-row row-content-block second-block" class="epics-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-xl-flex-direction-row gl-pb-3 row-content-block second-block"
> >
<gl-form-group class="mb-0"> <gl-dropdown
v-if="glFeatures.roadmapDaterangeFilter"
icon="calendar"
class="gl-mr-0 gl-lg-mr-3 mb-sm-2 roadmap-daterange-dropdown"
toggle-class="gl-rounded-base!"
:text="daterangeDropdownText"
data-testid="daterange-dropdown"
@show="handleDaterangeDropdownOpen"
@hide="handleDaterangeDropdownClose"
>
<gl-dropdown-item
v-for="dateRange in $options.availableDateRanges"
:key="dateRange.value"
:value="dateRange.value"
@click="handleDaterangeSelect(dateRange.value)"
>{{ dateRange.text }}</gl-dropdown-item
>
</gl-dropdown>
<gl-form-group v-if="availablePresets.length" class="gl-mr-0 gl-lg-mr-3 mb-sm-2">
<gl-segmented-control <gl-segmented-control
:checked="presetType" :checked="presetType"
:options="$options.availablePresets" :options="availablePresets"
class="gl-display-flex d-xl-block" class="gl-display-flex d-xl-block"
buttons buttons
@input="handleRoadmapLayoutChange" @input="handleRoadmapLayoutChange"
...@@ -112,8 +203,8 @@ export default { ...@@ -112,8 +203,8 @@ export default {
</gl-form-group> </gl-form-group>
<gl-dropdown <gl-dropdown
:text="selectedEpicStateTitle" :text="selectedEpicStateTitle"
class="gl-my-2 my-xl-0 mx-xl-2" class="gl-mr-0 gl-lg-mr-3 mb-sm-2"
toggle-class="gl-rounded-small" toggle-class="gl-rounded-base!"
> >
<gl-dropdown-item <gl-dropdown-item
:is-check-item="true" :is-check-item="true"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EXTEND_AS } from '../constants'; import { EXTEND_AS } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
milestonesListSection, milestonesListSection,
roadmapTimelineSection, roadmapTimelineSection,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -67,25 +69,28 @@ export default { ...@@ -67,25 +69,28 @@ export default {
methods: { methods: {
handleScroll() { handleScroll() {
const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el; 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 (!this.glFeatures.roadmapDaterangeFilter) {
if (isInViewport(timelineEdgeStartEl, { left: this.timeframeStartOffset })) { const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
this.$emit('onScrollToStart', { .querySelector('.timeline-header-item')
el: this.$refs.roadmapTimeline.$el, .querySelector('.item-sublabel .sublabel-value:first-child');
extendAs: EXTEND_AS.PREPEND, const timelineEdgeEndEl = this.$refs.roadmapTimeline.$el
}); .querySelector('.timeline-header-item:last-child')
} else if (isInViewport(timelineEdgeEndEl)) { .querySelector('.item-sublabel .sublabel-value:last-child');
// If timeline was scrolled to end
this.$emit('onScrollToEnd', { // If timeline was scrolled to start
el: this.$refs.roadmapTimeline.$el, if (isInViewport(timelineEdgeStartEl, { left: this.timeframeStartOffset })) {
extendAs: EXTEND_AS.APPEND, this.$emit('onScrollToStart', {
}); el: this.$refs.roadmapTimeline.$el,
extendAs: EXTEND_AS.PREPEND,
});
} else if (isInViewport(timelineEdgeEndEl)) {
// If timeline was scrolled to end
this.$emit('onScrollToEnd', {
el: this.$refs.roadmapTimeline.$el,
extendAs: EXTEND_AS.APPEND,
});
}
} }
eventHub.$emit('epicsListScrolled', { scrollTop, scrollLeft, clientHeight, scrollHeight }); eventHub.$emit('epicsListScrolled', { scrollTop, scrollLeft, clientHeight, scrollHeight });
......
...@@ -23,6 +23,12 @@ export const PERCENTAGE = 100; ...@@ -23,6 +23,12 @@ export const PERCENTAGE = 100;
export const SMALL_TIMELINE_BAR = 40; export const SMALL_TIMELINE_BAR = 40;
export const DATE_RANGES = {
CURRENT_QUARTER: 'CURRENT_QUARTER',
CURRENT_YEAR: 'CURRENT_YEAR',
THREE_YEARS: 'THREE_YEARS',
};
export const PRESET_TYPES = { export const PRESET_TYPES = {
QUARTERS: 'QUARTERS', QUARTERS: 'QUARTERS',
MONTHS: 'MONTHS', MONTHS: 'MONTHS',
......
...@@ -40,6 +40,8 @@ export default { ...@@ -40,6 +40,8 @@ export default {
sort: this.sortedBy, sort: this.sortedBy,
prev: this.prevPageCursor || undefined, prev: this.prevPageCursor || undefined,
next: this.nextPageCursor || undefined, next: this.nextPageCursor || undefined,
layout: this.presetType || undefined,
timeframe_range_type: this.timeframeRangeType || undefined,
author_username: authorUsername, author_username: authorUsername,
'label_name[]': labelName, 'label_name[]': labelName,
milestone_title: milestoneTitle, milestone_title: milestoneTitle,
......
...@@ -9,10 +9,14 @@ import EpicItem from './components/epic_item.vue'; ...@@ -9,10 +9,14 @@ import EpicItem from './components/epic_item.vue';
import EpicItemContainer from './components/epic_item_container.vue'; import EpicItemContainer from './components/epic_item_container.vue';
import roadmapApp from './components/roadmap_app.vue'; import roadmapApp from './components/roadmap_app.vue';
import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants'; import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH, DATE_RANGES } from './constants';
import createStore from './store'; import createStore from './store';
import { getTimeframeForPreset } from './utils/roadmap_utils'; import {
getTimeframeForPreset,
getPresetTypeForTimeframeRangeType,
getTimeframeForRangeType,
} from './utils/roadmap_utils';
Vue.use(Translate); Vue.use(Translate);
...@@ -57,18 +61,38 @@ export default () => { ...@@ -57,18 +61,38 @@ export default () => {
}; };
}, },
data() { data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
const presetType = let timeframe;
supportedPresetTypes.indexOf(dataset.presetType) > -1 let timeframeRangeType;
? dataset.presetType let presetType;
: PRESET_TYPES.MONTHS;
if (gon.features.roadmapDaterangeFilter) {
timeframeRangeType =
Object.keys(DATE_RANGES).indexOf(dataset.timeframeRangeType) > -1
? dataset.timeframeRangeType
: DATE_RANGES.CURRENT_QUARTER;
presetType = getPresetTypeForTimeframeRangeType(timeframeRangeType, dataset.presetType);
timeframe = getTimeframeForRangeType({
timeframeRangeType,
presetType,
});
} else {
presetType =
Object.keys(PRESET_TYPES).indexOf(dataset.presetType) > -1
? dataset.presetType
: PRESET_TYPES.MONTHS;
timeframe = getTimeframeForPreset(
presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
);
}
const rawFilterParams = queryToObject(window.location.search, { const rawFilterParams = queryToObject(window.location.search, {
gatherArrays: true, gatherArrays: true,
}); });
const filterParams = { const filterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, { ...convertObjectPropsToCamelCase(rawFilterParams, {
dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary dropKeys: ['scope', 'utf8', 'state', 'sort', 'timeframe_range_type', 'layout'], // These keys are unsupported/unnecessary
}), }),
// We shall put parsed value of `confidential` only // We shall put parsed value of `confidential` only
// when it is defined. // when it is defined.
...@@ -80,10 +104,6 @@ export default () => { ...@@ -80,10 +104,6 @@ export default () => {
epicIid: rawFilterParams.epicIid, epicIid: rawFilterParams.epicIid,
}), }),
}; };
const timeframe = getTimeframeForPreset(
presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
);
return { return {
emptyStateIllustrationPath: dataset.emptyStateIllustration, emptyStateIllustrationPath: dataset.emptyStateIllustration,
...@@ -98,6 +118,7 @@ export default () => { ...@@ -98,6 +118,7 @@ export default () => {
epicsState: dataset.epicsState, epicsState: dataset.epicsState,
sortedBy: dataset.sortedBy, sortedBy: dataset.sortedBy,
filterParams, filterParams,
timeframeRangeType,
presetType, presetType,
timeframe, timeframe,
}; };
...@@ -108,6 +129,7 @@ export default () => { ...@@ -108,6 +129,7 @@ export default () => {
fullPath: this.fullPath, fullPath: this.fullPath,
epicIid: this.epicIid, epicIid: this.epicIid,
sortedBy: this.sortedBy, sortedBy: this.sortedBy,
timeframeRangeType: this.timeframeRangeType,
presetType: this.presetType, presetType: this.presetType,
epicsState: this.epicsState, epicsState: this.epicsState,
timeframe: this.timeframe, timeframe: this.timeframe,
...@@ -125,6 +147,7 @@ export default () => { ...@@ -125,6 +147,7 @@ export default () => {
render(createElement) { render(createElement) {
return createElement('roadmap-app', { return createElement('roadmap-app', {
props: { props: {
timeframeRangeType: this.timeframeRangeType,
presetType: this.presetType, presetType: this.presetType,
emptyStateIllustrationPath: this.emptyStateIllustrationPath, emptyStateIllustrationPath: this.emptyStateIllustrationPath,
}, },
......
...@@ -17,6 +17,7 @@ export default () => ({ ...@@ -17,6 +17,7 @@ export default () => ({
timeframe: [], timeframe: [],
extendedTimeframe: [], extendedTimeframe: [],
presetType: '', presetType: '',
timeframeRangeType: '',
sortedBy: '', sortedBy: '',
milestoneIds: [], milestoneIds: [],
milestones: [], milestones: [],
......
...@@ -10,3 +10,10 @@ export const gqClient = createGqClient( ...@@ -10,3 +10,10 @@ export const gqClient = createGqClient(
export const addIsChildEpicTrueProperty = (obj) => ({ ...obj, isChildEpic: true }); export const addIsChildEpicTrueProperty = (obj) => ({ ...obj, isChildEpic: true });
export const generateKey = (epic) => `${epic.isChildEpic ? 'child-epic-' : 'epic-'}${epic.id}`; export const generateKey = (epic) => `${epic.isChildEpic ? 'child-epic-' : 'epic-'}${epic.id}`;
export const scrollToCurrentDay = (parentEl) => {
const todayIndicatorEl = parentEl.querySelector('.js-current-day-indicator');
if (todayIndicatorEl) {
todayIndicatorEl.scrollIntoView({ block: 'nearest', inline: 'center' });
}
};
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
DAYS_IN_WEEK, DAYS_IN_WEEK,
EXTEND_AS, EXTEND_AS,
PRESET_DEFAULTS, PRESET_DEFAULTS,
DATE_RANGES,
PRESET_TYPES, PRESET_TYPES,
TIMELINE_CELL_MIN_WIDTH, TIMELINE_CELL_MIN_WIDTH,
} from '../constants'; } from '../constants';
...@@ -364,6 +365,117 @@ export const getTimeframeForPreset = ( ...@@ -364,6 +365,117 @@ export const getTimeframeForPreset = (
return timeframe; return timeframe;
}; };
export const getWeeksForDates = (startDate, endDate) => {
const timeframe = [];
const start = newDate(startDate);
const end = newDate(endDate);
// Move to Sunday that comes just before startDate
start.setDate(start.getDate() - start.getDay());
while (start.getTime() < end.getTime()) {
// Push date to timeframe only when day is
// first day (Sunday) of the week
timeframe.push(newDate(start));
// Move date next Sunday
start.setDate(start.getDate() + DAYS_IN_WEEK);
}
return timeframe;
};
export const getTimeframeForRangeType = ({
timeframeRangeType = DATE_RANGES.CURRENT_QUARTER,
presetType = PRESET_TYPES.WEEKS,
}) => {
let timeframe = [];
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
// We need to prepare timeframe containing all the weeks of
// current quarter.
if (timeframeRangeType === DATE_RANGES.CURRENT_QUARTER) {
// 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(),
);
// Get last day of the last month of current quarter
const endDate = newDate(startDate);
if (currentMonthInCurrentQuarter === 0) {
endDate.setMonth(endDate.getMonth() + 2);
} else if (currentMonthInCurrentQuarter === 1) {
endDate.setMonth(endDate.getMonth() + 1);
}
endDate.setDate(totalDaysInMonth(endDate));
// Move startDate to first day of the first month of current quarter
startDate.setMonth(startDate.getMonth() - currentMonthInCurrentQuarter);
startDate.setDate(1);
timeframe = getWeeksForDates(startDate, endDate);
} else if (timeframeRangeType === DATE_RANGES.CURRENT_YEAR) {
// Move start date to first day of current year
startDate.setMonth(0);
startDate.setDate(1);
if (presetType === PRESET_TYPES.MONTHS) {
timeframe = getTimeframeWindowFrom(startDate, 12);
} else {
// Get last day of current year
const endDate = newDate(startDate);
endDate.setMonth(11);
endDate.setDate(totalDaysInMonth(endDate));
timeframe = getWeeksForDates(startDate, endDate);
}
} else {
// Get last day of the month, 18 months from startDate.
const endDate = newDate(startDate);
endDate.setMonth(endDate.getMonth() + 18);
endDate.setDate(totalDaysInMonth(endDate));
// Move start date to the 18 months behind
startDate.setMonth(startDate.getMonth() - 18);
startDate.setDate(1);
if (presetType === PRESET_TYPES.QUARTERS) {
timeframe = getTimeframeWindowFrom(startDate, 18 * 2);
const quartersTimeframe = [];
// Iterate over the timeframe and break it down
// in chunks of quarters
for (let i = 0; i < timeframe.length; i += 3) {
const range = timeframe.slice(i, i + 3);
const lastMonthOfQuarter = range[range.length - 1];
const quarterSequence = Math.floor((range[0].getMonth() + 3) / 3);
const year = range[0].getFullYear();
// Ensure that `range` spans across duration of
// entire quarter
lastMonthOfQuarter.setDate(totalDaysInMonth(lastMonthOfQuarter));
quartersTimeframe.push({
quarterSequence,
range,
year,
});
}
timeframe = quartersTimeframe;
} else if (presetType === PRESET_TYPES.MONTHS) {
timeframe = getTimeframeWindowFrom(startDate, 18 * 2);
} else {
timeframe = getWeeksForDates(startDate, endDate);
}
}
return timeframe;
};
/** /**
* Returns timeframe range in string based on provided config. * Returns timeframe range in string based on provided config.
* *
...@@ -440,3 +552,27 @@ export const sortEpics = (epics, sortedBy) => { ...@@ -440,3 +552,27 @@ export const sortEpics = (epics, sortedBy) => {
return 0; return 0;
}); });
}; };
export const getPresetTypeForTimeframeRangeType = (timeframeRangeType, initialPresetType) => {
let presetType;
switch (timeframeRangeType) {
case DATE_RANGES.CURRENT_QUARTER:
presetType = PRESET_TYPES.WEEKS;
break;
case DATE_RANGES.CURRENT_YEAR:
presetType = [PRESET_TYPES.MONTHS, PRESET_TYPES.WEEKS].includes(initialPresetType)
? initialPresetType
: PRESET_TYPES.MONTHS;
break;
case DATE_RANGES.THREE_YEARS:
presetType = [PRESET_TYPES.QUARTERS, PRESET_TYPES.MONTHS, PRESET_TYPES.WEEKS].includes(
initialPresetType,
)
? initialPresetType
: PRESET_TYPES.QUARTERS;
break;
default:
break;
}
return presetType;
};
...@@ -536,3 +536,16 @@ html.group-epics-roadmap-html { ...@@ -536,3 +536,16 @@ html.group-epics-roadmap-html {
color: var(--gray-500, $gray-500); color: var(--gray-500, $gray-500);
padding-top: $gl-spacing-scale-1; padding-top: $gl-spacing-scale-1;
} }
.epics-roadmap-filters {
.sort-dropdown-container {
// This override is needed to make sort-dropdown have same height
// as filtered search bar.
@include media-breakpoint-up(sm) {
.dropdown,
> button {
margin-bottom: $gl-padding-8;
}
}
}
}
...@@ -10,6 +10,7 @@ module Groups ...@@ -10,6 +10,7 @@ module Groups
before_action do before_action do
push_frontend_feature_flag(:async_filtering, @group, default_enabled: true) push_frontend_feature_flag(:async_filtering, @group, default_enabled: true)
push_frontend_feature_flag(:performance_roadmap, @group, default_enabled: :yaml) push_frontend_feature_flag(:performance_roadmap, @group, default_enabled: :yaml)
push_frontend_feature_flag(:roadmap_daterange_filter, @group, type: :development, default_enabled: :yaml)
end end
feature_category :roadmaps feature_category :roadmaps
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
epics_docs_path: help_page_path('user/group/epics/index'), epics_docs_path: help_page_path('user/group/epics/index'),
group_labels_endpoint: group_labels_path(@group, format: :json), group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json), group_milestones_endpoint: group_milestones_path(@group, format: :json),
timeframe_range_type: params[:timeframe_range_type],
preset_type: roadmap_layout, preset_type: roadmap_layout,
epics_state: @epics_state, epics_state: @epics_state,
sorted_by: @sort, sorted_by: @sort,
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from 'ee/roadmap/constants'; } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
import { REQUEST_EPICS_FOR_NEXT_PAGE } from 'ee/roadmap/store/mutation_types'; import { REQUEST_EPICS_FOR_NEXT_PAGE } from 'ee/roadmap/store/mutation_types';
import { scrollToCurrentDay } from 'ee/roadmap/utils/epic_utils';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { import {
mockFormattedChildEpic1, mockFormattedChildEpic1,
...@@ -24,6 +25,11 @@ import { ...@@ -24,6 +25,11 @@ import {
} from 'ee_jest/roadmap/mock_data'; } from 'ee_jest/roadmap/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
jest.mock('ee/roadmap/utils/epic_utils', () => ({
...jest.requireActual('ee/roadmap/utils/epic_utils'),
scrollToCurrentDay: jest.fn(),
}));
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const store = createStore(); const store = createStore();
store.dispatch('setInitialData', { store.dispatch('setInitialData', {
...@@ -166,8 +172,6 @@ describe('EpicsListSectionComponent', () => { ...@@ -166,8 +172,6 @@ describe('EpicsListSectionComponent', () => {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27992#note_319213990 // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27992#note_319213990
wrapper.destroy(); wrapper.destroy();
wrapper = createComponent(); wrapper = createComponent();
jest.spyOn(wrapper.vm, 'scrollToTodayIndicator').mockImplementation(() => {});
}); });
it('calls action `setBufferSize` with value based on window.innerHeight and component element position', () => { it('calls action `setBufferSize` with value based on window.innerHeight and component element position', () => {
...@@ -182,15 +186,12 @@ describe('EpicsListSectionComponent', () => { ...@@ -182,15 +186,12 @@ describe('EpicsListSectionComponent', () => {
}); });
}); });
it('calls `scrollToTodayIndicator` following the component render', () => { it('calls `scrollToCurrentDay` following the component render', async () => {
// Original method implementation waits for render cycle // Original method implementation waits for render cycle
// to complete at 2 levels before scrolling. // to complete at 2 levels before scrolling.
return wrapper.vm await wrapper.vm.$nextTick(); // set offsetLeft value
.$nextTick() await wrapper.vm.$nextTick(); // Wait for nextTick before scroll
.then(() => wrapper.vm.$nextTick()) expect(scrollToCurrentDay).toHaveBeenCalledWith(wrapper.vm.$el);
.then(() => {
expect(wrapper.vm.scrollToTodayIndicator).toHaveBeenCalled();
});
}); });
it('sets style object to `emptyRowContainerStyles`', () => { it('sets style object to `emptyRowContainerStyles`', () => {
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
TIMELINE_CELL_MIN_WIDTH, TIMELINE_CELL_MIN_WIDTH,
} from 'ee/roadmap/constants'; } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
import { scrollToCurrentDay } from 'ee/roadmap/utils/epic_utils';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { import {
mockTimeframeInitialDate, mockTimeframeInitialDate,
...@@ -16,6 +17,11 @@ import { ...@@ -16,6 +17,11 @@ import {
} from 'ee_jest/roadmap/mock_data'; } from 'ee_jest/roadmap/mock_data';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('ee/roadmap/utils/epic_utils', () => ({
...jest.requireActual('ee/roadmap/utils/epic_utils'),
scrollToCurrentDay: jest.fn(),
}));
const initializeStore = (mockTimeframeMonths) => { const initializeStore = (mockTimeframeMonths) => {
const store = createStore(); const store = createStore();
store.dispatch('setInitialData', { store.dispatch('setInitialData', {
...@@ -104,6 +110,14 @@ describe('MilestonesListSectionComponent', () => { ...@@ -104,6 +110,14 @@ describe('MilestonesListSectionComponent', () => {
it('sets value of `roadmapShellEl` with root component element', () => { it('sets value of `roadmapShellEl` with root component element', () => {
expect(wrapper.vm.roadmapShellEl instanceof HTMLElement).toBe(true); expect(wrapper.vm.roadmapShellEl instanceof HTMLElement).toBe(true);
}); });
it('calls `scrollToCurrentDay` following the component render', async () => {
// Original method implementation waits for render cycle
// to complete at 2 levels before scrolling.
await wrapper.vm.$nextTick(); // set offsetLeft value
await wrapper.vm.$nextTick(); // Wait for nextTick before scroll
expect(scrollToCurrentDay).toHaveBeenCalledWith(wrapper.vm.$el);
});
}); });
describe('handleEpicsListScroll', () => { describe('handleEpicsListScroll', () => {
......
import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue'; import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
import { PRESET_TYPES, EPICS_STATES } from 'ee/roadmap/constants'; import { PRESET_TYPES, EPICS_STATES, DATE_RANGES } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { import {
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
} from 'ee_jest/roadmap/mock_data'; } from 'ee_jest/roadmap/mock_data';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
...@@ -37,6 +38,8 @@ const createComponent = ({ ...@@ -37,6 +38,8 @@ const createComponent = ({
groupMilestonesPath = '/groups/gitlab-org/-/milestones.json', groupMilestonesPath = '/groups/gitlab-org/-/milestones.json',
timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate), timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate),
filterParams = {}, filterParams = {},
roadmapDaterangeFilter = false,
timeframeRangeType = DATE_RANGES.CURRENT_QUARTER,
} = {}) => { } = {}) => {
const localVue = createLocalVue(); const localVue = createLocalVue();
const store = createStore(); const store = createStore();
...@@ -51,13 +54,19 @@ const createComponent = ({ ...@@ -51,13 +54,19 @@ const createComponent = ({
timeframe, timeframe,
}); });
return shallowMount(RoadmapFilters, { return shallowMountExtended(RoadmapFilters, {
localVue, localVue,
store, store,
provide: { provide: {
groupFullPath, groupFullPath,
groupMilestonesPath, groupMilestonesPath,
listEpicsPath, listEpicsPath,
glFeatures: {
roadmapDaterangeFilter,
},
},
props: {
timeframeRangeType,
}, },
}); });
}; };
...@@ -106,13 +115,17 @@ describe('RoadmapFilters', () => { ...@@ -106,13 +115,17 @@ describe('RoadmapFilters', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(global.window.location.href).toBe( expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true`, `${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&layout=MONTHS&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true`,
); );
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
const quarters = { text: 'Quarters', value: PRESET_TYPES.QUARTERS };
const months = { text: 'Months', value: PRESET_TYPES.MONTHS };
const weeks = { text: 'Weeks', value: PRESET_TYPES.WEEKS };
beforeEach(() => { beforeEach(() => {
updateHistory({ url: TEST_HOST, title: document.title, replace: true }); updateHistory({ url: TEST_HOST, title: document.title, replace: true });
}); });
...@@ -122,11 +135,7 @@ describe('RoadmapFilters', () => { ...@@ -122,11 +135,7 @@ describe('RoadmapFilters', () => {
expect(layoutSwitches.exists()).toBe(true); expect(layoutSwitches.exists()).toBe(true);
expect(layoutSwitches.props('checked')).toBe(PRESET_TYPES.MONTHS); expect(layoutSwitches.props('checked')).toBe(PRESET_TYPES.MONTHS);
expect(layoutSwitches.props('options')).toEqual([ expect(layoutSwitches.props('options')).toEqual([quarters, months, weeks]);
{ text: 'Quarters', value: PRESET_TYPES.QUARTERS },
{ text: 'Months', value: PRESET_TYPES.MONTHS },
{ text: 'Weeks', value: PRESET_TYPES.WEEKS },
]);
}); });
it('switching layout using roadmap layout switching buttons causes page to reload with selected layout', () => { it('switching layout using roadmap layout switching buttons causes page to reload with selected layout', () => {
...@@ -302,5 +311,63 @@ describe('RoadmapFilters', () => { ...@@ -302,5 +311,63 @@ describe('RoadmapFilters', () => {
}); });
}); });
}); });
describe('when roadmapDaterangeFilter feature flag is enabled', () => {
let wrapperWithDaterangeFilter;
const availableRanges = [
{ text: 'This quarter', value: DATE_RANGES.CURRENT_QUARTER },
{ text: 'This year', value: DATE_RANGES.CURRENT_YEAR },
{ text: 'Within 3 years', value: DATE_RANGES.THREE_YEARS },
];
beforeEach(async () => {
wrapperWithDaterangeFilter = createComponent({
roadmapDaterangeFilter: true,
timeframeRangeType: DATE_RANGES.CURRENT_QUARTER,
});
await wrapperWithDaterangeFilter.vm.$nextTick();
});
afterEach(() => {
wrapperWithDaterangeFilter.destroy();
});
it('renders daterange dropdown', async () => {
wrapperWithDaterangeFilter.setData({ selectedDaterange: DATE_RANGES.CURRENT_QUARTER });
await wrapperWithDaterangeFilter.vm.$nextTick();
const daterangeDropdown = wrapperWithDaterangeFilter.findByTestId('daterange-dropdown');
expect(daterangeDropdown.exists()).toBe(true);
expect(daterangeDropdown.props('text')).toBe('This quarter');
daterangeDropdown.findAllComponents(GlDropdownItem).wrappers.forEach((item, index) => {
expect(item.text()).toBe(availableRanges[index].text);
expect(item.attributes('value')).toBe(availableRanges[index].value);
});
});
it.each`
selectedDaterange | availablePresets
${DATE_RANGES.CURRENT_QUARTER} | ${[]}
${DATE_RANGES.CURRENT_YEAR} | ${[months, weeks]}
${DATE_RANGES.THREE_YEARS} | ${[quarters, months, weeks]}
`(
'renders $availablePresets.length items when selected daterange is "$selectedDaterange"',
async ({ selectedDaterange, availablePresets }) => {
wrapperWithDaterangeFilter.setData({ selectedDaterange });
await wrapperWithDaterangeFilter.vm.$nextTick();
const layoutSwitches = wrapperWithDaterangeFilter.findComponent(GlSegmentedControl);
if (selectedDaterange === DATE_RANGES.CURRENT_QUARTER) {
expect(layoutSwitches.exists()).toBe(false);
} else {
expect(layoutSwitches.exists()).toBe(true);
expect(layoutSwitches.props('options')).toEqual(availablePresets);
}
},
);
});
}); });
}); });
...@@ -44,3 +44,20 @@ describe('generateKey', () => { ...@@ -44,3 +44,20 @@ describe('generateKey', () => {
expect(epicUtils.generateKey(obj)).toBe('child-epic-3'); expect(epicUtils.generateKey(obj)).toBe('child-epic-3');
}); });
}); });
describe('scrollToCurrentDay', () => {
it('scrolls current day indicator into view', () => {
const currentDayIndicator = document.createElement('div');
currentDayIndicator.classList.add('js-current-day-indicator');
document.body.appendChild(currentDayIndicator);
jest.spyOn(currentDayIndicator, 'scrollIntoView').mockImplementation();
epicUtils.scrollToCurrentDay(document.body);
expect(currentDayIndicator.scrollIntoView).toHaveBeenCalledWith({
block: 'nearest',
inline: 'center',
});
});
});
import { PRESET_TYPES } from 'ee/roadmap/constants'; import { PRESET_TYPES, DATE_RANGES } from 'ee/roadmap/constants';
import { import {
getTimeframeForQuartersView, getTimeframeForQuartersView,
extendTimeframeForQuartersView, extendTimeframeForQuartersView,
...@@ -8,7 +8,10 @@ import { ...@@ -8,7 +8,10 @@ import {
extendTimeframeForWeeksView, extendTimeframeForWeeksView,
extendTimeframeForAvailableWidth, extendTimeframeForAvailableWidth,
getEpicsTimeframeRange, getEpicsTimeframeRange,
getWeeksForDates,
getTimeframeForRangeType,
sortEpics, sortEpics,
getPresetTypeForTimeframeRangeType,
} from 'ee/roadmap/utils/roadmap_utils'; } from 'ee/roadmap/utils/roadmap_utils';
import { import {
...@@ -25,6 +28,7 @@ import { ...@@ -25,6 +28,7 @@ import {
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate); const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const getDateString = (date) => date.toISOString().split('T')[0];
describe('getTimeframeForQuartersView', () => { describe('getTimeframeForQuartersView', () => {
let timeframe; let timeframe;
...@@ -295,6 +299,113 @@ describe('extendTimeframeForAvailableWidth', () => { ...@@ -295,6 +299,113 @@ describe('extendTimeframeForAvailableWidth', () => {
}); });
}); });
describe('getWeeksForDates', () => {
it('returns weeks for given dates', () => {
const weeks = getWeeksForDates(mockTimeframeInitialDate, mockTimeframeMonths[4]);
expect(weeks).toHaveLength(9);
expect(getDateString(weeks[0])).toBe('2017-12-31');
expect(getDateString(weeks[4])).toBe('2018-01-28');
expect(getDateString(weeks[8])).toBe('2018-02-25');
});
});
describe('getTimeframeForRangeType', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2021-01-01'));
});
afterEach(() => {
jest.useFakeTimers('legacy');
jest.runOnlyPendingTimers();
});
it('returns timeframe with weeks when timeframeRangeType is current quarter', () => {
const timeframe = getTimeframeForRangeType({ timeframeRangeType: DATE_RANGES.CURRENT_QUARTER });
expect(timeframe).toHaveLength(14);
expect(getDateString(timeframe[0])).toBe('2020-12-27');
expect(getDateString(timeframe[6])).toBe('2021-02-07');
expect(getDateString(timeframe[13])).toBe('2021-03-28');
});
it('returns timeframe with months when timeframeRangeType is current year and preset type is months', () => {
const timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.CURRENT_YEAR,
presetType: PRESET_TYPES.MONTHS,
});
expect(timeframe).toHaveLength(12);
expect(getDateString(timeframe[0])).toBe('2021-01-01');
expect(getDateString(timeframe[5])).toBe('2021-06-01');
expect(getDateString(timeframe[11])).toBe('2021-12-31');
});
it('returns timeframe with weeks when timeframeRangeType is current year', () => {
const timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.CURRENT_YEAR,
presetType: PRESET_TYPES.WEEKS,
});
expect(timeframe).toHaveLength(53);
expect(getDateString(timeframe[0])).toBe('2020-12-27');
expect(getDateString(timeframe[25])).toBe('2021-06-20');
expect(getDateString(timeframe[52])).toBe('2021-12-26');
});
it('returns timeframe with quarters when timeframeRangeType is within 3 years', () => {
const timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.THREE_YEARS,
presetType: PRESET_TYPES.QUARTERS,
});
expect(timeframe).toHaveLength(12);
expect(timeframe[0]).toMatchObject({
quarterSequence: 3,
year: 2019,
range: expect.any(Array),
});
expect(getDateString(timeframe[0].range[0])).toBe('2019-07-01');
expect(getDateString(timeframe[0].range[1])).toBe('2019-08-01');
expect(getDateString(timeframe[0].range[2])).toBe('2019-09-30');
expect(timeframe[11]).toMatchObject({
quarterSequence: 2,
year: 2022,
range: expect.any(Array),
});
expect(getDateString(timeframe[11].range[0])).toBe('2022-04-01');
expect(getDateString(timeframe[11].range[1])).toBe('2022-05-01');
expect(getDateString(timeframe[11].range[2])).toBe('2022-06-30');
});
it('returns timeframe with months when timeframeRangeType is within 3 years', () => {
const timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.THREE_YEARS,
presetType: PRESET_TYPES.MONTHS,
});
expect(timeframe).toHaveLength(36);
expect(getDateString(timeframe[0])).toBe('2019-07-01');
expect(getDateString(timeframe[35])).toBe('2022-06-30');
});
it('returns timeframe with weeks when timeframeRangeType is within 3 years', () => {
const timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.THREE_YEARS,
presetType: PRESET_TYPES.WEEKS,
});
expect(timeframe).toHaveLength(161);
expect(getDateString(timeframe[0])).toBe('2019-06-30');
expect(getDateString(timeframe[160])).toBe('2022-07-24');
});
});
describe('getEpicsTimeframeRange', () => { describe('getEpicsTimeframeRange', () => {
it('returns object containing startDate and dueDate based on provided timeframe for Quarters', () => { it('returns object containing startDate and dueDate based on provided timeframe for Quarters', () => {
const timeframeQuarters = getTimeframeForQuartersView(new Date(2018, 0, 1)); const timeframeQuarters = getTimeframeForQuartersView(new Date(2018, 0, 1));
...@@ -437,3 +548,17 @@ describe('sortEpics', () => { ...@@ -437,3 +548,17 @@ describe('sortEpics', () => {
}); });
}); });
}); });
describe('getPresetTypeForTimeframeRangeType', () => {
it.each`
timeframeRangeType | presetType
${DATE_RANGES.CURRENT_QUARTER} | ${PRESET_TYPES.WEEKS}
${DATE_RANGES.CURRENT_YEAR} | ${PRESET_TYPES.MONTHS}
${DATE_RANGES.THREE_YEARS} | ${PRESET_TYPES.QUARTERS}
`(
'returns presetType as $presetType when $timeframeRangeType',
({ timeframeRangeType, presetType }) => {
expect(getPresetTypeForTimeframeRangeType(timeframeRangeType)).toEqual(presetType);
},
);
});
...@@ -15905,6 +15905,12 @@ msgstr "" ...@@ -15905,6 +15905,12 @@ msgstr ""
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline" msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
msgstr "" msgstr ""
msgid "GroupRoadmap|This quarter"
msgstr ""
msgid "GroupRoadmap|This year"
msgstr ""
msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them." msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them."
msgstr "" msgstr ""
...@@ -15917,6 +15923,9 @@ msgstr "" ...@@ -15917,6 +15923,9 @@ msgstr ""
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}." msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
msgstr "" msgstr ""
msgid "GroupRoadmap|Within 3 years"
msgstr ""
msgid "GroupSAML|%{strongOpen}Warning%{strongClose} - Enabling %{linkStart}SSO enforcement%{linkEnd} can reduce security risks." msgid "GroupSAML|%{strongOpen}Warning%{strongClose} - Enabling %{linkStart}SSO enforcement%{linkEnd} can reduce security risks."
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