Commit 4a0a2b10 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added Top N labels request api spec

Minor cleanup linting

Added action specs for fetchTopRankedGroupLabels

Added mutation specs for top ranked labels

Updated tests with cases for testing
the top ranked labels request
parent 08cc02f8
......@@ -475,7 +475,6 @@ img.emoji {
.mw-90p { max-width: 90%; }
.min-height-0 { min-height: 0; }
.min-width-300 { min-height: 300px; }
.svg-w-100 {
svg {
......
......@@ -113,7 +113,7 @@ export default {
startDate,
endDate,
selectedProjectIds,
tasksByType: { subject, labelIds: selectedLabelIds },
tasksByType: { subject, selectedLabelIds },
} = this;
return {
selectedGroup,
......
......@@ -132,8 +132,8 @@ export default {
<icon :size="16" name="settings" />
<icon :size="16" name="chevron-down" />
</gl-button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right min-width-300">
<div class="js-tasks-by-type-chart-filters-subject mb-3 mx-3">
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right">
<div class="js-tasks-by-type-chart-filters-subject mb-3 px-3">
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<gl-segmented-control
v-model="selectedSubjectFilter"
......@@ -145,7 +145,7 @@ export default {
/>
</div>
<gl-dropdown-divider />
<div class="js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100">
<div class="js-tasks-by-type-chart-filters-labels mb-3 px-3">
<p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }}
<br />
......@@ -155,7 +155,7 @@ export default {
<input class="dropdown-input-field" type="search" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content px-0 w-100"></div>
<div class="dropdown-content px-0"></div>
</div>
</div>
</div>
......
......@@ -270,7 +270,7 @@ export const receiveTopRankedGroupLabelsSuccess = ({ commit }, data) =>
export const receiveTopRankedGroupLabelsError = ({ commit }, error) => {
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR, error);
createFlash(__('There was an error fetching label data for the selected group'));
createFlash(__('There was an error fetching the top labels for the selected group'));
};
export const requestTopRankedGroupLabels = ({ commit }) =>
......@@ -409,22 +409,22 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
const {
currentGroupPath,
cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
topRankedLabelIds,
} = getters;
const {
tasksByType: { labelIds, subject },
tasksByType: { subject },
} = state;
// dont request if we have no labels selected...for now
if (labelIds.length) {
if (topRankedLabelIds.length) {
const params = {
group_id: currentGroupPath,
created_after,
created_before,
project_ids,
subject,
label_ids: labelIds,
label_ids: topRankedLabelIds,
};
dispatch('requestTasksByTypeData');
......
......@@ -11,6 +11,9 @@ export const currentGroupPath = ({ selectedGroup }) =>
export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects.length ? selectedProjects.map(({ id }) => id) : [];
export const topRankedLabelIds = ({ topRankedLabels }) =>
topRankedLabels.length ? topRankedLabels.map(({ id }) => id) : [];
export const cycleAnalyticsRequestParams = ({ startDate = null, endDate = null }, getters) => ({
project_ids: getters.selectedProjectIds,
created_after: startDate ? dateFormat(startDate, dateFormats.isoDate) : null,
......
......@@ -69,16 +69,15 @@ export default {
state.topRankedLabels = [];
state.tasksByType = {
...state.tasksByType,
labelIds: [],
selectedLabelIds: [],
};
},
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS](state, data = []) {
const { tasksByType } = state;
state.topRankedLabels = data.length ? data.map(convertObjectPropsToCamelCase) : [];
// TODO: Should probably append as many labels from `labels` as we need if we dont get enough returned
state.tasksByType = {
...tasksByType,
labelIds: data.length ? data.map(({ id }) => id) : [],
selectedLabelIds: data.length ? data.map(({ id }) => id) : [],
};
},
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR](state) {
......@@ -86,7 +85,7 @@ export default {
state.topRankedLabels = [];
state.tasksByType = {
...tasksByType,
labelIds: [],
selectedLabelIds: [],
};
},
[types.REQUEST_STAGE_MEDIANS](state) {
......
......@@ -37,7 +37,7 @@ export default () => ({
tasksByType: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
labelIds: [],
selectedLabelIds: [],
data: [],
},
......
......@@ -12,19 +12,19 @@ exports[`TasksByTypeChart no data available should render the no data available
`;
exports[`TasksByTypeChart with data available filters labels has label filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100\\">
"<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 px-3\\">
<p class=\\"font-weight-bold text-left my-2\\">
Select labels
<br> <small>3 selected (15 max)</small></p>
<div class=\\"dropdown-input px-0\\"><input type=\\"search\\" class=\\"dropdown-input-field\\"> <svg aria-hidden=\\"true\\" class=\\"dropdown-input-search s16 ic-search\\" data-hidden=\\"true\\">
<use xlink:href=\\"#search\\"></use>
</svg></div>
<div class=\\"dropdown-content px-0 w-100\\"></div>
<div class=\\"dropdown-content px-0\\"></div>
</div>"
`;
exports[`TasksByTypeChart with data available filters labels with label dropdown open renders the group labels as dropdown items 1`] = `
"<div class=\\"dropdown-content px-0 w-100\\">
"<div class=\\"dropdown-content px-0\\">
<ul>
<li>
<a href=\\"#\\" class=\\"dropdown-menu-link \\">
......@@ -54,7 +54,7 @@ exports[`TasksByTypeChart with data available filters labels with label dropdown
`;
exports[`TasksByTypeChart with data available filters subject has subject filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 mx-3\\">
"<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<div role=\\"radiogroup\\" tabindex=\\"-1\\" class=\\"gl-segmented-control btn-group-toggle btn-group\\" id=\\"__BVID__74\\"><label class=\\"btn btn-gl-segmented-button active\\"><input id=\\"__BVID__74__BV_option_0_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"Issue\\"><span>Issues</span></label><label class=\\"btn btn-gl-segmented-button\\"><input id=\\"__BVID__74__BV_option_1_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"MergeRequest\\"><span>Merge Requests</span></label></div>
</div>"
......
......@@ -6,7 +6,10 @@ import testAction from 'helpers/vuex_action_helper';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { TASKS_BY_TYPE_FILTERS } from 'ee/analytics/cycle_analytics/constants';
import {
TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE,
} from 'ee/analytics/cycle_analytics/constants';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { toYmd } from 'ee/analytics/shared/utils';
......@@ -332,6 +335,72 @@ describe('Cycle analytics actions', () => {
});
});
describe('fetchTopRankedGroupLabels', () => {
beforeEach(() => {
gon.api_version = 'v4';
state = { selectedGroup, tasksByType: { subject: TASKS_BY_TYPE_SUBJECT_ISSUE } };
});
describe('succeeds', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeTopLabelsData).replyOnce(200, groupLabels);
});
it('dispatches receiveTopRankedGroupLabelsSuccess if the request succeeds', () => {
const dispatch = jest.fn();
return actions
.fetchTopRankedGroupLabels({
dispatch,
state,
getters,
})
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestTopRankedGroupLabels');
expect(dispatch).toHaveBeenCalledWith(
'receiveTopRankedGroupLabelsSuccess',
groupLabels,
);
});
});
});
describe('with an error', () => {
beforeEach(() => {
mock.onGet(endpoints.fetchTopRankedGroupLabels).replyOnce(404);
});
it('dispatches receiveTopRankedGroupLabelsError if the request fails', () => {
const dispatch = jest.fn();
return actions
.fetchTopRankedGroupLabels({
dispatch,
state,
getters,
})
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestTopRankedGroupLabels');
expect(dispatch).toHaveBeenCalledWith('receiveTopRankedGroupLabelsError', error);
});
});
});
describe('receiveTopRankedGroupLabelsError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('flashes an error message if the request fails', () => {
actions.receiveTopRankedGroupLabelsError({
commit: () => {},
});
shouldFlashAMessage('There was an error fetching the top labels for the selected group');
});
});
});
describe('fetchCycleAnalyticsData', () => {
function mockFetchCycleAnalyticsAction(overrides = {}) {
const mocks = {
......
......@@ -8,6 +8,7 @@ import {
durationChartPlottableMedianData,
allowedStages,
selectedProjects,
groupLabels,
} from '../mock_data';
let state = null;
......@@ -30,6 +31,44 @@ describe('Cycle analytics getters', () => {
});
});
describe('selectedProjectIds', () => {
describe('with selectedProjects set', () => {
it('returns the ids of each project', () => {
state = {
selectedProjects,
};
expect(getters.selectedProjectIds(state)).toEqual(selectedProjects.map(({ id }) => id));
});
});
describe('without selectedProjects set', () => {
it('will return an empty array', () => {
state = { selectedProjects: [] };
expect(getters.selectedProjectIds(state)).toEqual([]);
});
});
});
describe('topRankedLabelIds', () => {
describe('with topRankedLabels set', () => {
it('returns the `fullPath` value of the group', () => {
state = {
topRankedLabels: groupLabels,
};
expect(getters.topRankedLabelIds(state)).toEqual(groupLabels.map(({ id }) => id));
});
});
describe('without topRankedLabels set', () => {
it('will return an empty array', () => {
state = { topRankedLabels: [] };
expect(getters.topRankedLabelIds(state)).toEqual([]);
});
});
});
describe('currentGroupPath', () => {
describe('with selectedGroup set', () => {
it('returns the `fullPath` value of the group', () => {
......
......@@ -53,6 +53,8 @@ describe('Cycle analytics mutations', () => {
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]}
${types.REQUEST_TOP_RANKED_GROUP_LABELS} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]}
......@@ -154,6 +156,14 @@ describe('Cycle analytics mutations', () => {
});
});
describe(`${types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS}`, () => {
it('will set the labels state item with the camelCased group labels', () => {
mutations[types.RECEIVE_GROUP_LABELS_SUCCESS](state, groupLabels);
expect(state.labels).toEqual(groupLabels.map(convertObjectPropsToCamelCase));
});
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......
......@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import Api from 'ee/api';
import * as cycleAnalyticsConstants from 'ee/analytics/cycle_analytics/constants';
import axios from '~/lib/utils/axios_utils';
import * as analyticsMockData from 'ee_jest/analytics/cycle_analytics/mock_data';
describe('Api', () => {
const dummyApiVersion = 'v3000';
......@@ -353,13 +354,38 @@ describe('Api', () => {
subject: cycleAnalyticsConstants.TASKS_BY_TYPE_SUBJECT_ISSUE,
label_ids: labelIds,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/type_of_work/tasks_by_type`;
const expectedUrl = analyticsMockData.endpoints.tasksByTypeData;
mock.onGet(expectedUrl).reply(200, tasksByTypeResponse);
Api.cycleAnalyticsTasksByType({ params })
Api.cycleAnalyticsTasksByType(params)
.then(({ data, config: { params: reqParams } }) => {
expect(data).toEqual(tasksByTypeResponse);
expect(reqParams.params).toEqual(params);
expect(reqParams).toEqual(params);
})
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsTopLabels', () => {
it('fetches top group level labels', done => {
const response = [];
const labelIds = [10, 9, 8, 7];
const params = {
...defaultParams,
project_ids: null,
subject: cycleAnalyticsConstants.TASKS_BY_TYPE_SUBJECT_ISSUE,
label_ids: labelIds,
};
const expectedUrl = analyticsMockData.endpoints.tasksByTypeTopLabelsData;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsTopLabels(params)
.then(({ data, config: { url, params: reqParams } }) => {
expect(data).toEqual(response);
expect(url).toMatch(expectedUrl);
expect(reqParams).toEqual(params);
})
.then(done)
.catch(done.fail);
......
......@@ -6110,6 +6110,9 @@ msgstr ""
msgid "CycleAnalyticsStage|should be under a group"
msgstr ""
msgid "CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)"
msgstr ""
msgid "CycleAnalytics|%{stageCount} stages selected"
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