Commit 12e1ac89 authored by Kushal Pandya's avatar Kushal Pandya Committed by Felipe Artur

Add support for fetch Roadmap epics via GraphQL

Fetches epics in Roadmap via Graph query including
support for filters.
parent 080efbba
......@@ -177,6 +177,15 @@ export const urlParamsToArray = (path = '') =>
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
/**
* Accepts encoding string which includes query params being
* sent to URL.
*
* @param {string} path Query param string
*
* @returns {object} Query params object containing key-value pairs
* with both key and values decoded into plain string.
*/
export const urlParamsToObject = (path = '') =>
splitPath(path).reduce((dataParam, filterParam) => {
if (filterParam === '') {
......@@ -185,6 +194,7 @@ export const urlParamsToObject = (path = '') =>
const data = dataParam;
let [key, value] = filterParam.split('=');
key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
const isArray = key.includes('[]');
key = key.replace('[]', '');
value = decodeURIComponent(value.replace(/\+/g, ' '));
......
......@@ -32,8 +32,16 @@ export default {
},
},
data() {
const roadmapGraphQL = gon.features && gon.features.roadmapGraphql;
return {
handleResizeThrottled: {},
// TODO
// Remove these method alias and call actual
// method once feature flag is removed.
fetchEpicsFn: roadmapGraphQL ? this.fetchEpicsGQL : this.fetchEpics,
fetchEpicsForTimeframeFn: roadmapGraphQL
? this.fetchEpicsForTimeframeGQL
: this.fetchEpicsForTimeframe,
};
},
computed: {
......@@ -78,7 +86,7 @@ export default {
},
},
mounted() {
this.fetchEpics();
this.fetchEpicsFn();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
......@@ -89,7 +97,9 @@ export default {
...mapActions([
'setWindowResizeInProgress',
'fetchEpics',
'fetchEpicsGQL',
'fetchEpicsForTimeframe',
'fetchEpicsForTimeframeGQL',
'extendTimeframe',
'refreshEpicDates',
]),
......@@ -146,7 +156,7 @@ export default {
this.refreshEpicDates();
this.$nextTick(() => {
this.fetchEpicsForTimeframe({
this.fetchEpicsForTimeframeFn({
timeframe: this.extendedTimeframe,
})
.then(() => {
......
query epicChildEpics(
$fullPath: ID!
$iid: ID!
$state: EpicState
$sort: EpicSort
$startDate: Time
$dueDate: Time
) {
group(fullPath: $fullPath) {
id
name
epic(iid: $iid) {
id
title
children(state: $state, sort: $sort, startDate: $startDate, endDate: $dueDate) {
edges {
node {
id
title
state
webUrl
startDate
dueDate
group {
name
fullName
}
}
}
}
}
}
}
query groupEpics(
$fullPath: ID!
$state: EpicState
$sort: EpicSort
$startDate: Time
$dueDate: Time
$labelName: [String!] = []
$authorUsername: String = ""
$search: String = ""
) {
group(fullPath: $fullPath) {
id
name
epics(
state: $state
sort: $sort
startDate: $startDate
endDate: $dueDate
labelName: $labelName
authorUsername: $authorUsername
search: $search
) {
edges {
node {
id
title
state
webUrl
startDate
dueDate
group {
name
fullName
}
}
}
}
}
}
......@@ -3,12 +3,16 @@ import { mapActions } from 'vuex';
import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
parseBoolean,
urlParamsToObject,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { visitUrl, mergeUrlParams } from '~/lib/utils/url_utility';
import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants';
import { getTimeframeForPreset, getEpicsPathForPreset } from './utils/roadmap_utils';
import { getEpicsPathForPreset, getTimeframeForPreset } from './utils/roadmap_utils';
import createStore from './store';
......@@ -48,6 +52,11 @@ export default () => {
? dataset.presetType
: PRESET_TYPES.MONTHS;
const filterQueryString = window.location.search.substring(1);
const filterParams = Object.assign(
convertObjectPropsToCamelCase(urlParamsToObject(filterQueryString), {
dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary
}),
);
const timeframe = getTimeframeForPreset(
presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
......@@ -66,12 +75,15 @@ export default () => {
defaultInnerHeight: Number(dataset.innerHeight),
isChildEpics: parseBoolean(dataset.childEpics),
currentGroupId: parseInt(dataset.groupId, 0),
basePath: dataset.epicsPath,
fullPath: dataset.fullPath,
epicIid: dataset.iid,
newEpicEndpoint: dataset.newEpicEndpoint,
epicsState: dataset.epicsState,
basePath: dataset.epicsPath,
sortedBy: dataset.sortedBy,
filterQueryString,
initialEpicsPath,
filterParams,
presetType,
timeframe,
};
......@@ -79,11 +91,15 @@ export default () => {
created() {
this.setInitialData({
currentGroupId: this.currentGroupId,
fullPath: this.fullPath,
epicIid: this.epicIid,
sortedBy: this.sortedBy,
presetType: this.presetType,
epicsState: this.epicsState,
timeframe: this.timeframe,
basePath: this.basePath,
filterQueryString: this.filterQueryString,
filterParams: this.filterParams,
initialEpicsPath: this.initialEpicsPath,
defaultInnerHeight: this.defaultInnerHeight,
isChildEpics: this.isChildEpics,
......
......@@ -3,9 +3,17 @@ import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import * as epicUtils from '../utils/epic_utils';
import { getEpicsPathForPreset, sortEpics, extendTimeframeForPreset } from '../utils/roadmap_utils';
import {
getEpicsPathForPreset,
getEpicsTimeframeRange,
sortEpics,
extendTimeframeForPreset,
} from '../utils/roadmap_utils';
import { EXTEND_AS } from '../constants';
import groupEpics from '../queries/groupEpics.query.graphql';
import epicChildEpics from '../queries/epicChildEpics.query.graphql';
import * as types from './mutation_types';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
......@@ -13,6 +21,54 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT
export const setWindowResizeInProgress = ({ commit }, inProgress) =>
commit(types.SET_WINDOW_RESIZE_IN_PROGRESS, inProgress);
export const fetchGroupEpics = (
{ epicIid, fullPath, epicsState, sortedBy, presetType, filterParams, timeframe },
defaultTimeframe,
) => {
let query;
let variables = {
fullPath,
state: epicsState,
sort: sortedBy,
...getEpicsTimeframeRange({
presetType,
timeframe: defaultTimeframe || timeframe,
}),
};
// When epicIid is present,
// Roadmap is being accessed from within an Epic,
// and then we don't need to pass `filterParams`.
if (epicIid) {
query = epicChildEpics;
variables.iid = epicIid;
} else {
query = groupEpics;
variables = {
...variables,
...filterParams,
};
}
return epicUtils.gqClient
.query({
query,
variables,
})
.then(({ data }) => {
const { group } = data;
let edges;
if (epicIid) {
edges = (group.epic && group.epic.children.edges) || [];
} else {
edges = (group.epics && group.epics.edges) || [];
}
return epicUtils.extractGroupEpics(edges);
});
};
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const requestEpicsForTimeframe = ({ commit }) => commit(types.REQUEST_EPICS_FOR_TIMEFRAME);
export const receiveEpicsSuccess = (
......@@ -60,9 +116,17 @@ export const fetchEpics = ({ state, dispatch }) => {
.then(({ data }) => {
dispatch('receiveEpicsSuccess', { rawEpics: data });
})
.catch(() => {
dispatch('receiveEpicsFailure');
});
.catch(() => dispatch('receiveEpicsFailure'));
};
export const fetchEpicsGQL = ({ state, dispatch }) => {
dispatch('requestEpics');
fetchGroupEpics(state)
.then(rawEpics => {
dispatch('receiveEpicsSuccess', { rawEpics });
})
.catch(() => dispatch('receiveEpicsFailure'));
};
export const fetchEpicsForTimeframe = ({ state, dispatch }, { timeframe }) => {
......@@ -90,6 +154,20 @@ export const fetchEpicsForTimeframe = ({ state, dispatch }, { timeframe }) => {
});
};
export const fetchEpicsForTimeframeGQL = ({ state, dispatch }, { timeframe }) => {
dispatch('requestEpicsForTimeframe');
return fetchGroupEpics(state, timeframe)
.then(rawEpics => {
dispatch('receiveEpicsSuccess', {
rawEpics,
newEpic: true,
timeframeExtended: true,
});
})
.catch(() => dispatch('receiveEpicsFailure'));
};
export const extendTimeframe = ({ commit, state, getters }, { extendAs }) => {
const isExtendTypePrepend = extendAs === EXTEND_AS.PREPEND;
......
......@@ -4,11 +4,14 @@ export default () => ({
epicsState: '',
filterQueryString: '',
initialEpicsPath: '',
filterParams: null,
// Data
epicIid: '',
epics: [],
epicIds: [],
currentGroupId: -1,
fullPath: '',
timeframe: [],
extendedTimeframe: [],
presetType: '',
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import createGqClient from '~/lib/graphql';
export const gqClient = createGqClient();
/**
* Updates provided `epic` object with necessary props
......@@ -71,10 +74,12 @@ export const processEpicDates = (epic, timeframeStartDate, timeframeEndDate) =>
*/
export const formatEpicDetails = (rawEpic, timeframeStartDate, timeframeEndDate) => {
const epicItem = convertObjectPropsToCamelCase(rawEpic);
const rawStartDate = rawEpic.start_date || rawEpic.startDate;
const rawEndDate = rawEpic.end_date || rawEpic.dueDate;
if (rawEpic.start_date) {
if (rawStartDate) {
// If startDate is present
const startDate = parsePikadayDate(rawEpic.start_date);
const startDate = parsePikadayDate(rawStartDate);
epicItem.startDate = startDate;
epicItem.originalStartDate = startDate;
} else {
......@@ -82,9 +87,9 @@ export const formatEpicDetails = (rawEpic, timeframeStartDate, timeframeEndDate)
epicItem.startDateUndefined = true;
}
if (rawEpic.end_date) {
if (rawEndDate) {
// If endDate is present
const endDate = parsePikadayDate(rawEpic.end_date);
const endDate = parsePikadayDate(rawEndDate);
epicItem.endDate = endDate;
epicItem.originalEndDate = endDate;
} else {
......@@ -96,3 +101,19 @@ export const formatEpicDetails = (rawEpic, timeframeStartDate, timeframeEndDate)
return epicItem;
};
/**
* Returns array of epics extracted from GraphQL response
* discarding the `edges`->`node` nesting
*
* @param {Object} group
*/
export const extractGroupEpics = edges =>
edges.map(({ node, epicNode = node }) => ({
...epicNode,
// We can get rid of below two lines
// by updating `epic_item_details.vue`
// once we move to GraphQL permanently.
groupName: epicNode.group.name,
groupFullName: epicNode.group.fullName,
}));
......@@ -366,6 +366,46 @@ export const getTimeframeForPreset = (
return timeframe;
};
/**
* Returns timeframe range in string based on provided config.
*
* @param {object} config
* @param {string} config.presetType String representing preset type
* @param {array} config.timeframe Array of dates representing timeframe
*
* @returns {object} Returns an object containing `startDate` & `dueDate` strings
* Computed using presetType and timeframe.
*/
export const getEpicsTimeframeRange = ({ presetType = '', timeframe = [] }) => {
let start;
let due;
const firstTimeframe = timeframe[0];
const lastTimeframe = timeframe[timeframe.length - 1];
// Compute start and end dates from timeframe
// based on provided presetType.
if (presetType === PRESET_TYPES.QUARTERS) {
[start] = firstTimeframe.range;
due = lastTimeframe.range[lastTimeframe.range.length - 1];
} else if (presetType === PRESET_TYPES.MONTHS) {
start = firstTimeframe;
due = lastTimeframe;
} else if (presetType === PRESET_TYPES.WEEKS) {
start = firstTimeframe;
due = newDate(lastTimeframe);
due.setDate(due.getDate() + 6);
}
const startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
const dueDate = `${due.getFullYear()}-${due.getMonth() + 1}-${due.getDate()}`;
return {
startDate,
dueDate,
};
};
export const getEpicsPathForPreset = ({
basePath = '',
filterQueryString = '',
......@@ -373,37 +413,19 @@ export const getEpicsPathForPreset = ({
timeframe = [],
epicsState = 'all',
}) => {
let start;
let end;
let epicsPath = basePath;
if (!basePath || !timeframe.length) {
return null;
}
const firstTimeframe = timeframe[0];
const lastTimeframe = timeframe[timeframe.length - 1];
// Construct Epic API path to include
// `start_date` & `end_date` query params to get list of
// epics only for timeframe.
if (presetType === PRESET_TYPES.QUARTERS) {
[start] = firstTimeframe.range;
end = lastTimeframe.range[lastTimeframe.range.length - 1];
} else if (presetType === PRESET_TYPES.MONTHS) {
start = firstTimeframe;
end = lastTimeframe;
} else if (presetType === PRESET_TYPES.WEEKS) {
start = firstTimeframe;
end = newDate(lastTimeframe);
end.setDate(end.getDate() + 6);
}
const range = getEpicsTimeframeRange({
presetType,
timeframe,
});
epicsPath += epicsPath.indexOf('?') === -1 ? '?' : '&';
const startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
const endDate = `${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`;
epicsPath += `state=${epicsState}&start_date=${startDate}&end_date=${endDate}`;
epicsPath += `state=${epicsState}&start_date=${range.startDate}&end_date=${range.dueDate}`;
if (filterQueryString) {
epicsPath += `&${filterQueryString}`;
......
......@@ -17,6 +17,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:epic_trees, @group)
push_frontend_feature_flag(:roadmap_graphql, @group)
end
def index
......
......@@ -8,6 +8,9 @@ module Groups
before_action :check_epics_available!
before_action :group
before_action :persist_roadmap_layout, only: [:show]
before_action do
push_frontend_feature_flag(:roadmap_graphql, @group)
end
# show roadmap for a group
def show
......
......@@ -39,6 +39,8 @@
%section.col-md-12
#js-roadmap{ data: { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json),
group_id: @group.id,
iid: @epic.iid,
full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: 'false',
new_epic_endpoint: group_epics_path(@group),
......@@ -88,6 +90,8 @@
%section.col-md-12
#js-roadmap{ data: { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json),
group_id: @group.id,
iid: @epic.iid,
full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: 'false',
new_epic_endpoint: group_epics_path(@group),
......
......@@ -9,6 +9,6 @@
- if @epics_count != 0
= render 'shared/epic/search_bar', type: :epics, show_roadmap_presets: true, hide_extra_sort_options: true
#js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), has_filters_applied: "#{has_filters_applied}", new_epic_endpoint: group_epics_path(@group), preset_type: roadmap_layout, epics_state: @epics_state, sorted_by: @sort } }
#js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, full_path: @group.full_path, empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), has_filters_applied: "#{has_filters_applied}", new_epic_endpoint: group_epics_path(@group), preset_type: roadmap_layout, epics_state: @epics_state, sorted_by: @sort } }
- else
= render 'shared/empty_states/roadmap'
......@@ -19,6 +19,7 @@ describe 'Epic show', :js do
end
let(:epic) { create(:epic, group: group, title: epic_title, description: markdown, author: user) }
let!(:not_child) { create(:epic, group: group, title: 'not child epic', description: markdown, author: user, start_date: 50.days.ago, end_date: 10.days.ago) }
let!(:child_epic_a) { create(:epic, group: group, title: 'Child epic A', description: markdown, parent: epic, start_date: 50.days.ago, end_date: 10.days.ago) }
let!(:child_epic_b) { create(:epic, group: group, title: 'Child epic B', description: markdown, parent: epic, start_date: 100.days.ago, end_date: 20.days.ago) }
......@@ -85,6 +86,7 @@ describe 'Epic show', :js do
expect(page).to have_selector('.roadmap-container .roadmap-shell')
page.within('.roadmap-shell .epics-list-section') do
expect(page).not_to have_content(not_child.title)
expect(find('.epics-list-item:nth-child(1) .epic-title a')).to have_content('Child epic B')
expect(find('.epics-list-item:nth-child(2) .epic-title a')).to have_content('Child epic A')
end
......
......@@ -62,6 +62,46 @@ describe('Roadmap AppComponent', () => {
it('returns default data props', () => {
expect(vm.handleResizeThrottled).toBeDefined();
});
describe('when `gon.feature.roadmapGraphql` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { roadmapGraphql: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('returns data prop containing `fetchEpicsFn` mapped to `fetchEpicsGQL`', () => {
expect(vm.fetchEpicsFn).toBe(vm.fetchEpicsGQL);
});
it('returns data prop containing `fetchEpicsForTimeframeFn` mapped to `fetchEpicsForTimeframeGQL`', () => {
expect(vm.fetchEpicsForTimeframeFn).toBe(vm.fetchEpicsForTimeframeGQL);
});
});
describe('when `gon.feature.roadmapGraphql` is false', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { roadmapGraphql: false };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('returns data prop containing `fetchEpicsFn` mapped to `fetchEpics`', () => {
expect(vm.fetchEpicsFn).toBe(vm.fetchEpics);
});
it('returns data prop containing `fetchEpicsForTimeframeFn` mapped to `fetchEpicsForTimeframe`', () => {
expect(vm.fetchEpicsForTimeframeFn).toBe(vm.fetchEpicsForTimeframe);
});
});
});
describe('computed', () => {
......@@ -192,7 +232,7 @@ describe('Roadmap AppComponent', () => {
it('calls `fetchEpicsForTimeframe` with extended timeframe array', done => {
spyOn(vm, 'extendTimeframe').and.stub();
spyOn(vm, 'refreshEpicDates').and.stub();
spyOn(vm, 'fetchEpicsForTimeframe').and.callFake(() => new Promise(() => {}));
spyOn(vm, 'fetchEpicsForTimeframeFn').and.callFake(() => new Promise(() => {}));
const extendType = EXTEND_AS.PREPEND;
......@@ -200,7 +240,7 @@ describe('Roadmap AppComponent', () => {
vm.$nextTick()
.then(() => {
expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith(
expect(vm.fetchEpicsForTimeframeFn).toHaveBeenCalledWith(
jasmine.objectContaining({
timeframe: vm.extendedTimeframe,
}),
......
......@@ -305,3 +305,58 @@ export const mockUnsortedEpics = [
endDate: new Date(2015, 7, 15),
},
];
export const mockGroupEpicsQueryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/2',
name: 'Gitlab Org',
epics: {
edges: [
{
node: {
id: 'gid://gitlab/Epic/40',
title: 'Marketing epic',
startDate: '2017-12-25',
dueDate: '2018-03-09',
webUrl: '/groups/gitlab-org/marketing/-/epics/1',
group: {
name: 'Gitlab Org',
fullName: 'Gitlab Org',
},
},
},
{
node: {
id: 'gid://gitlab/Epic/41',
title: 'Another marketing',
startDate: '2017-12-26',
dueDate: '2018-03-10',
webUrl: '/groups/gitlab-org/marketing/-/epics/2',
group: {
name: 'Gitlab Org',
fullName: 'Gitlab Org',
},
},
},
],
},
},
},
};
export const mockEpicChildEpicsQueryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/2',
name: 'Gitlab Org',
epic: {
id: 'gid://gitlab/Epic/1',
title: 'Error omnis quos consequatur',
children: {
edges: mockGroupEpicsQueryResponse.data.group.epics.edges,
},
},
},
},
};
......@@ -5,8 +5,10 @@ import * as types from 'ee/roadmap/store/mutation_types';
import defaultState from 'ee/roadmap/store/state';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
import { formatEpicDetails } from 'ee/roadmap/utils/epic_utils';
import * as epicUtils from 'ee/roadmap/utils/epic_utils';
import { PRESET_TYPES, EXTEND_AS } from 'ee/roadmap/constants';
import groupEpics from 'ee/roadmap/queries/groupEpics.query.graphql';
import epicChildEpics from 'ee/roadmap/queries/epicChildEpics.query.graphql';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper';
......@@ -22,6 +24,8 @@ import {
mockRawEpic,
mockFormattedEpic,
mockSortedBy,
mockGroupEpicsQueryResponse,
mockEpicChildEpicsQueryResponse,
} from '../mock_data';
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
......@@ -76,6 +80,73 @@ describe('Roadmap Vuex Actions', () => {
});
});
describe('fetchGroupEpics', () => {
let mockState;
let expectedVariables;
beforeEach(() => {
mockState = {
fullPath: 'gitlab-org',
epicsState: 'all',
sortedBy: 'start_date_asc',
presetType: PRESET_TYPES.MONTHS,
filterParams: {},
timeframe: mockTimeframeMonths,
};
expectedVariables = {
fullPath: 'gitlab-org',
state: mockState.epicsState,
sort: mockState.sortedBy,
startDate: '2017-11-1',
dueDate: '2018-6-30',
};
});
it('should fetch Group Epics using GraphQL client when epicIid is not present in state', done => {
spyOn(epicUtils.gqClient, 'query').and.returnValue(
Promise.resolve({
data: mockGroupEpicsQueryResponse.data,
}),
);
actions
.fetchGroupEpics(mockState)
.then(() => {
expect(epicUtils.gqClient.query).toHaveBeenCalledWith({
query: groupEpics,
variables: expectedVariables,
});
})
.then(done)
.catch(done.fail);
});
it('should fetch child Epics of an Epic using GraphQL client when epicIid is present in state', done => {
spyOn(epicUtils.gqClient, 'query').and.returnValue(
Promise.resolve({
data: mockEpicChildEpicsQueryResponse.data,
}),
);
mockState.epicIid = '1';
actions
.fetchGroupEpics(mockState)
.then(() => {
expect(epicUtils.gqClient.query).toHaveBeenCalledWith({
query: epicChildEpics,
variables: {
iid: '1',
...expectedVariables,
},
});
})
.then(done)
.catch(done.fail);
});
});
describe('requestEpics', () => {
it('Should set `epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: 'REQUEST_EPICS' }], [], done);
......@@ -315,7 +386,7 @@ describe('Roadmap Vuex Actions', () => {
describe('refreshEpicDates', () => {
it('Should update epics after refreshing epic dates to match with updated timeframe', done => {
const epics = rawEpics.map(epic =>
formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
epicUtils.formatEpicDetails(epic, state.timeframeStartDate, state.timeframeEndDate),
);
testAction(
......
......@@ -2,7 +2,7 @@ import * as epicUtils from 'ee/roadmap/utils/epic_utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import { rawEpics } from '../mock_data';
import { rawEpics, mockGroupEpicsQueryResponse } from '../mock_data';
describe('processEpicDates', () => {
const timeframeStartDate = new Date(2017, 0, 1);
......@@ -89,3 +89,19 @@ describe('formatEpicDetails', () => {
expect(epic.endDateUndefined).toBe(true);
});
});
describe('extractGroupEpics', () => {
it('returns array of epics with `edges->nodes` nesting removed', () => {
const { edges } = mockGroupEpicsQueryResponse.data.group.epics;
const extractedEpics = epicUtils.extractGroupEpics(edges);
expect(extractedEpics.length).toBe(edges.length);
expect(extractedEpics[0]).toEqual(
jasmine.objectContaining({
...edges[0].node,
groupName: edges[0].node.group.name,
groupFullName: edges[0].node.group.fullName,
}),
);
});
});
......@@ -6,9 +6,10 @@ import {
getTimeframeForWeeksView,
extendTimeframeForWeeksView,
extendTimeframeForAvailableWidth,
getEpicsTimeframeRange,
getEpicsPathForPreset,
sortEpics,
assignDates,
sortEpics,
} from 'ee/roadmap/utils/roadmap_utils';
import { PRESET_TYPES } from 'ee/roadmap/constants';
......@@ -297,6 +298,53 @@ describe('extendTimeframeForAvailableWidth', () => {
});
});
describe('getEpicsTimeframeRange', () => {
it('returns object containing startDate and dueDate based on provided timeframe for Quarters', () => {
const timeframeQuarters = getTimeframeForQuartersView(new Date(2018, 0, 1));
const range = getEpicsTimeframeRange({
presetType: PRESET_TYPES.QUARTERS,
timeframe: timeframeQuarters,
});
expect(range).toEqual(
jasmine.objectContaining({
startDate: '2017-7-1',
dueDate: '2019-3-31',
}),
);
});
it('returns object containing startDate and dueDate based on provided timeframe for Months', () => {
const timeframeMonths = getTimeframeForMonthsView(new Date(2018, 0, 1));
const range = getEpicsTimeframeRange({
presetType: PRESET_TYPES.MONTHS,
timeframe: timeframeMonths,
});
expect(range).toEqual(
jasmine.objectContaining({
startDate: '2017-11-1',
dueDate: '2018-6-30',
}),
);
});
it('returns object containing startDate and dueDate based on provided timeframe for Weeks', () => {
const timeframeWeeks = getTimeframeForWeeksView(new Date(2018, 0, 1));
const range = getEpicsTimeframeRange({
presetType: PRESET_TYPES.WEEKS,
timeframe: timeframeWeeks,
});
expect(range).toEqual(
jasmine.objectContaining({
startDate: '2017-12-17',
dueDate: '2018-2-3',
}),
);
});
});
describe('getEpicsPathForPreset', () => {
const basePath = '/groups/gitlab-org/-/epics.json';
const filterQueryString = 'scope=all&utf8=✓&state=opened&label_name[]=Bug';
......
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