Skip to content
Snippets Groups Projects
Commit 48318f75 authored by Kushal Pandya's avatar Kushal Pandya Committed by Olena Horal-Koretska
Browse files

Add daterange picker to filter Roadmap

parent e2e50bb9
No related branches found
No related tags found
No related merge requests found
Showing
with 466 additions and 76 deletions
---
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
Loading
Loading
@@ -34,6 +34,6 @@ export default {
<span
v-if="hasToday"
:style="indicatorStyles"
class="current-day-indicator position-absolute"
class="current-day-indicator js-current-day-indicator gl-absolute"
></span>
</template>
Loading
Loading
@@ -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 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 EpicItem from './epic_item.vue';
Loading
Loading
@@ -115,7 +115,7 @@ export default {
// to timeline expand, so we wait for another render
// cycle to complete.
this.$nextTick(() => {
this.scrollToTodayIndicator();
scrollToCurrentDay(this.$el);
});
 
if (!Object.keys(this.emptyRowContainerStyles).length) {
Loading
Loading
@@ -139,13 +139,6 @@ export default {
}
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 }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
Loading
Loading
Loading
Loading
@@ -4,6 +4,7 @@ import { mapState, mapActions } from 'vuex';
import { __, n__ } from '~/locale';
import { EPIC_DETAILS_CELL_WIDTH, EPIC_ITEM_HEIGHT, TIMELINE_CELL_MIN_WIDTH } from '../constants';
import eventHub from '../event_hub';
import { scrollToCurrentDay } from '../utils/epic_utils';
import MilestoneTimeline from './milestone_timeline.vue';
 
const EXPAND_BUTTON_EXPANDED = {
Loading
Loading
@@ -97,13 +98,10 @@ export default {
this.offsetLeft = (this.$el.parentElement && this.$el.parentElement.offsetLeft) || 0;
 
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 }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
Loading
Loading
Loading
Loading
@@ -9,6 +9,7 @@ import {
EXTEND_AS,
EPICS_LIMIT_DISMISSED_COOKIE_NAME,
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT,
DATE_RANGES,
} from '../constants';
import eventHub from '../event_hub';
import EpicsListEmpty from './epics_list_empty.vue';
Loading
Loading
@@ -32,6 +33,11 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
timeframeRangeType: {
type: String,
required: false,
default: DATE_RANGES.CURRENT_QUARTER,
},
presetType: {
type: String,
required: true,
Loading
Loading
@@ -155,7 +161,7 @@ export default {
 
<template>
<div class="roadmap-app-container gl-h-full">
<roadmap-filters v-if="showFilteredSearchbar" />
<roadmap-filters v-if="showFilteredSearchbar" :timeframe-range-type="timeframeRangeType" />
<gl-alert
v-if="isWarningVisible"
variant="warning"
Loading
Loading
Loading
Loading
@@ -9,18 +9,26 @@ import {
import { mapState, mapActions } from 'vuex';
 
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 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 { getPresetTypeForTimeframeRangeType } from '../utils/roadmap_utils';
const pickerType = {
Start: 'start',
End: 'end',
};
 
export default {
pickerType,
epicStates: EPICS_STATES,
availablePresets: [
{ text: __('Quarters'), value: PRESET_TYPES.QUARTERS },
{ text: __('Months'), value: PRESET_TYPES.MONTHS },
{ text: __('Weeks'), value: PRESET_TYPES.WEEKS },
availableDateRanges: [
{ text: s__('GroupRoadmap|This quarter'), value: DATE_RANGES.CURRENT_QUARTER },
{ text: s__('GroupRoadmap|This year'), value: DATE_RANGES.CURRENT_YEAR },
{ text: s__('GroupRoadmap|Within 3 years'), value: DATE_RANGES.THREE_YEARS },
],
availableSortOptions: [
{
Loading
Loading
@@ -48,7 +56,18 @@ export default {
GlDropdownDivider,
FilteredSearchBar,
},
mixins: [EpicsFilteredSearchMixin],
mixins: [EpicsFilteredSearchMixin, glFeatureFlagsMixin()],
props: {
timeframeRangeType: {
type: String,
required: true,
},
},
data() {
return {
selectedDaterange: this.timeframeRangeType,
};
},
computed: {
...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams']),
selectedEpicStateTitle() {
Loading
Loading
@@ -59,6 +78,34 @@ export default {
}
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: {
urlParams: {
Loading
Loading
@@ -77,8 +124,34 @@ export default {
},
methods: {
...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) {
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) {
this.setEpicsState(epicsState);
Loading
Loading
@@ -99,12 +172,30 @@ export default {
<template>
<div class="epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui">
<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
:checked="presetType"
:options="$options.availablePresets"
:options="availablePresets"
class="gl-display-flex d-xl-block"
buttons
@input="handleRoadmapLayoutChange"
Loading
Loading
@@ -112,8 +203,8 @@ export default {
</gl-form-group>
<gl-dropdown
:text="selectedEpicStateTitle"
class="gl-my-2 my-xl-0 mx-xl-2"
toggle-class="gl-rounded-small"
class="gl-mr-0 gl-lg-mr-3 mb-sm-2"
toggle-class="gl-rounded-base!"
>
<gl-dropdown-item
:is-check-item="true"
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@
import { mapState } from 'vuex';
 
import { isInViewport } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EXTEND_AS } from '../constants';
import eventHub from '../event_hub';
 
Loading
Loading
@@ -15,6 +16,7 @@ export default {
milestonesListSection,
roadmapTimelineSection,
},
mixins: [glFeatureFlagsMixin()],
props: {
presetType: {
type: String,
Loading
Loading
@@ -67,25 +69,28 @@ export default {
methods: {
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', {
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,
});
if (!this.glFeatures.roadmapDaterangeFilter) {
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', {
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 });
Loading
Loading
Loading
Loading
@@ -23,6 +23,12 @@ export const PERCENTAGE = 100;
 
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 = {
QUARTERS: 'QUARTERS',
MONTHS: 'MONTHS',
Loading
Loading
Loading
Loading
@@ -40,6 +40,8 @@ export default {
sort: this.sortedBy,
prev: this.prevPageCursor || undefined,
next: this.nextPageCursor || undefined,
layout: this.presetType || undefined,
timeframe_range_type: this.timeframeRangeType || undefined,
author_username: authorUsername,
'label_name[]': labelName,
milestone_title: milestoneTitle,
Loading
Loading
Loading
Loading
@@ -9,10 +9,14 @@ import EpicItem from './components/epic_item.vue';
import EpicItemContainer from './components/epic_item_container.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 { getTimeframeForPreset } from './utils/roadmap_utils';
import {
getTimeframeForPreset,
getPresetTypeForTimeframeRangeType,
getTimeframeForRangeType,
} from './utils/roadmap_utils';
 
Vue.use(Translate);
 
Loading
Loading
@@ -57,18 +61,38 @@ export default () => {
};
},
data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el;
const presetType =
supportedPresetTypes.indexOf(dataset.presetType) > -1
? dataset.presetType
: PRESET_TYPES.MONTHS;
let timeframe;
let timeframeRangeType;
let presetType;
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, {
gatherArrays: true,
});
const filterParams = {
...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
// when it is defined.
Loading
Loading
@@ -80,10 +104,6 @@ export default () => {
epicIid: rawFilterParams.epicIid,
}),
};
const timeframe = getTimeframeForPreset(
presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
);
 
return {
emptyStateIllustrationPath: dataset.emptyStateIllustration,
Loading
Loading
@@ -98,6 +118,7 @@ export default () => {
epicsState: dataset.epicsState,
sortedBy: dataset.sortedBy,
filterParams,
timeframeRangeType,
presetType,
timeframe,
};
Loading
Loading
@@ -108,6 +129,7 @@ export default () => {
fullPath: this.fullPath,
epicIid: this.epicIid,
sortedBy: this.sortedBy,
timeframeRangeType: this.timeframeRangeType,
presetType: this.presetType,
epicsState: this.epicsState,
timeframe: this.timeframe,
Loading
Loading
@@ -125,6 +147,7 @@ export default () => {
render(createElement) {
return createElement('roadmap-app', {
props: {
timeframeRangeType: this.timeframeRangeType,
presetType: this.presetType,
emptyStateIllustrationPath: this.emptyStateIllustrationPath,
},
Loading
Loading
Loading
Loading
@@ -17,6 +17,7 @@ export default () => ({
timeframe: [],
extendedTimeframe: [],
presetType: '',
timeframeRangeType: '',
sortedBy: '',
milestoneIds: [],
milestones: [],
Loading
Loading
Loading
Loading
@@ -10,3 +10,10 @@ export const gqClient = createGqClient(
export const addIsChildEpicTrueProperty = (obj) => ({ ...obj, isChildEpic: true });
 
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' });
}
};
Loading
Loading
@@ -4,6 +4,7 @@ import {
DAYS_IN_WEEK,
EXTEND_AS,
PRESET_DEFAULTS,
DATE_RANGES,
PRESET_TYPES,
TIMELINE_CELL_MIN_WIDTH,
} from '../constants';
Loading
Loading
@@ -364,6 +365,117 @@ export const getTimeframeForPreset = (
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.
*
Loading
Loading
@@ -440,3 +552,27 @@ export const sortEpics = (epics, sortedBy) => {
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;
};
Loading
Loading
@@ -536,3 +536,16 @@ html.group-epics-roadmap-html {
color: var(--gray-500, $gray-500);
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;
}
}
}
}
Loading
Loading
@@ -10,6 +10,7 @@ class RoadmapController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:async_filtering, @group, default_enabled: true)
push_frontend_feature_flag(:performance_roadmap, @group, default_enabled: :yaml)
push_frontend_feature_flag(:roadmap_daterange_filter, @group, type: :development, default_enabled: :yaml)
end
 
feature_category :roadmaps
Loading
Loading
Loading
Loading
@@ -25,6 +25,7 @@
epics_docs_path: help_page_path('user/group/epics/index'),
group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json),
timeframe_range_type: params[:timeframe_range_type],
preset_type: roadmap_layout,
epics_state: @epics_state,
sorted_by: @sort,
Loading
Loading
Loading
Loading
@@ -10,6 +10,7 @@ import {
} from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
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 {
mockFormattedChildEpic1,
Loading
Loading
@@ -24,6 +25,11 @@ import {
} from 'ee_jest/roadmap/mock_data';
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 store = createStore();
store.dispatch('setInitialData', {
Loading
Loading
@@ -166,8 +172,6 @@ describe('EpicsListSectionComponent', () => {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27992#note_319213990
wrapper.destroy();
wrapper = createComponent();
jest.spyOn(wrapper.vm, 'scrollToTodayIndicator').mockImplementation(() => {});
});
 
it('calls action `setBufferSize` with value based on window.innerHeight and component element position', () => {
Loading
Loading
@@ -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
// to complete at 2 levels before scrolling.
return wrapper.vm
.$nextTick()
.then(() => wrapper.vm.$nextTick())
.then(() => {
expect(wrapper.vm.scrollToTodayIndicator).toHaveBeenCalled();
});
await wrapper.vm.$nextTick(); // set offsetLeft value
await wrapper.vm.$nextTick(); // Wait for nextTick before scroll
expect(scrollToCurrentDay).toHaveBeenCalledWith(wrapper.vm.$el);
});
 
it('sets style object to `emptyRowContainerStyles`', () => {
Loading
Loading
Loading
Loading
@@ -8,6 +8,7 @@ import {
TIMELINE_CELL_MIN_WIDTH,
} from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { scrollToCurrentDay } from 'ee/roadmap/utils/epic_utils';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import {
mockTimeframeInitialDate,
Loading
Loading
@@ -16,6 +17,11 @@ import {
} from 'ee_jest/roadmap/mock_data';
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 store = createStore();
store.dispatch('setInitialData', {
Loading
Loading
@@ -104,6 +110,14 @@ describe('MilestonesListSectionComponent', () => {
it('sets value of `roadmapShellEl` with root component element', () => {
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', () => {
Loading
Loading
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 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 { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import {
Loading
Loading
@@ -18,6 +18,7 @@ import {
} from 'ee_jest/roadmap/mock_data';
 
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 FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
 
Loading
Loading
@@ -37,6 +38,8 @@ const createComponent = ({
groupMilestonesPath = '/groups/gitlab-org/-/milestones.json',
timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate),
filterParams = {},
roadmapDaterangeFilter = false,
timeframeRangeType = DATE_RANGES.CURRENT_QUARTER,
} = {}) => {
const localVue = createLocalVue();
const store = createStore();
Loading
Loading
@@ -51,13 +54,19 @@ const createComponent = ({
timeframe,
});
 
return shallowMount(RoadmapFilters, {
return shallowMountExtended(RoadmapFilters, {
localVue,
store,
provide: {
groupFullPath,
groupMilestonesPath,
listEpicsPath,
glFeatures: {
roadmapDaterangeFilter,
},
},
props: {
timeframeRangeType,
},
});
};
Loading
Loading
@@ -106,13 +115,17 @@ describe('RoadmapFilters', () => {
await wrapper.vm.$nextTick();
 
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', () => {
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(() => {
updateHistory({ url: TEST_HOST, title: document.title, replace: true });
});
Loading
Loading
@@ -122,11 +135,7 @@ describe('RoadmapFilters', () => {
 
expect(layoutSwitches.exists()).toBe(true);
expect(layoutSwitches.props('checked')).toBe(PRESET_TYPES.MONTHS);
expect(layoutSwitches.props('options')).toEqual([
{ text: 'Quarters', value: PRESET_TYPES.QUARTERS },
{ text: 'Months', value: PRESET_TYPES.MONTHS },
{ text: 'Weeks', value: PRESET_TYPES.WEEKS },
]);
expect(layoutSwitches.props('options')).toEqual([quarters, months, weeks]);
});
 
it('switching layout using roadmap layout switching buttons causes page to reload with selected layout', () => {
Loading
Loading
@@ -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);
}
},
);
});
});
});
Loading
Loading
@@ -44,3 +44,20 @@ describe('generateKey', () => {
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',
});
});
});
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