Commit b85f9cc8 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'mw-cr-filter-store' into 'master'

Code Review Analytics: Add filters store

See merge request gitlab-org/gitlab!30361
parents f97b60b3 9fe94507
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const setMilestonesEndpoint = ({ commit }, milestonesEndpoint) =>
commit(types.SET_MILESTONES_ENDPOINT, milestonesEndpoint);
export const setLabelsEndpoint = ({ commit }, labelsEndpoint) =>
commit(types.SET_LABELS_ENDPOINT, labelsEndpoint);
export const fetchMilestones = ({ commit, state }) => {
commit(types.REQUEST_MILESTONES);
return axios
.get(state.milestonesEndpoint)
.then(({ data }) => {
commit(types.RECEIVE_MILESTONES_SUCCESS, data);
})
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
createFlash(__('Failed to load milestones. Please try again.'));
});
};
export const fetchLabels = ({ commit, state }) => {
commit(types.REQUEST_LABELS);
return axios
.get(state.labelsEndpoint)
.then(({ data }) => {
commit(types.RECEIVE_LABELS_SUCCESS, data);
})
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
createFlash(__('Failed to load labels. Please try again.'));
});
};
export const setFilters = ({ commit, dispatch }, { label_name, milestone_title }) => {
commit(types.SET_FILTERS, { labelName: label_name, milestoneTitle: milestone_title });
commit(types.SET_FILTERS, { selectedLabels: label_name, selectedMilestone: milestone_title });
dispatch('mergeRequests/setPage', 1, { root: true });
dispatch('mergeRequests/fetchMergeRequests', null, { root: true });
......
// eslint-disable-next-line import/prefer-default-export
export const SET_MILESTONES_ENDPOINT = 'SET_MILESTONES_ENDPOINT';
export const SET_LABELS_ENDPOINT = 'SET_LABELS_ENDPOINT';
export const REQUEST_MILESTONES = 'REQUEST_MILESTONES';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_ERROR = 'RECEIVE_MILESTONES_ERROR';
export const REQUEST_LABELS = 'REQUEST_LABELS';
export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const RECEIVE_LABELS_ERROR = 'RECEIVE_LABELS_ERROR';
export const SET_FILTERS = 'SET_FILTERS';
import * as types from './mutation_types';
export default {
[types.SET_FILTERS](state, { labelName, milestoneTitle }) {
state.labelName = labelName;
state.milestoneTitle = milestoneTitle;
[types.SET_MILESTONES_ENDPOINT](state, milestonesEndpoint) {
state.milestonesEndpoint = milestonesEndpoint;
},
[types.SET_LABELS_ENDPOINT](state, labelsEndpoint) {
state.labelsEndpoint = labelsEndpoint;
},
[types.REQUEST_MILESTONES](state) {
state.milestones.isLoading = true;
},
[types.RECEIVE_MILESTONES_SUCCESS](state, data) {
state.milestones.isLoading = false;
state.milestones.data = data;
state.milestones.errorCode = null;
},
[types.RECEIVE_MILESTONES_ERROR](state, errorCode) {
state.milestones.isLoading = false;
state.milestones.errorCode = errorCode;
state.milestones.data = [];
},
[types.REQUEST_LABELS](state) {
state.labels.isLoading = true;
},
[types.RECEIVE_LABELS_SUCCESS](state, data) {
state.labels.isLoading = false;
state.labels.data = data;
state.labels.errorCode = null;
},
[types.RECEIVE_LABELS_ERROR](state, errorCode) {
state.labels.isLoading = false;
state.labels.errorCode = errorCode;
state.labels.data = [];
},
[types.SET_FILTERS](state, { selectedLabels, selectedMilestone }) {
state.labels.selected = selectedLabels;
state.milestones.selected = selectedMilestone;
},
};
export default () => ({
labelName: [],
milestoneTitle: null,
milestonePath: '',
labelsPath: '',
milestones: {
isLoading: false,
data: [],
errorCode: null,
selected: null,
},
labels: {
isLoading: false,
data: [],
errorCode: null,
selected: [],
},
});
......@@ -11,7 +11,8 @@ export const fetchMergeRequests = ({ dispatch, state, rootState }) => {
const { projectId, pageInfo } = state;
const { milestoneTitle, labelName } = rootState.filters;
const { selected: milestoneTitle } = rootState.filters.milestones;
const { selected: labelName } = rootState.filters.labels;
const params = {
project_id: projectId,
......
......@@ -2,6 +2,6 @@ export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS';
export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS';
export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERRORR';
export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR';
export const SET_PAGE = 'SET_PAGE';
......@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { GlTable } from '@gitlab/ui';
import MergeRequestTable from 'ee/analytics/code_review_analytics/components/merge_request_table.vue';
import createState from 'ee/analytics/code_review_analytics/store/modules/merge_requests/state';
import mergeRequests from '../mock_data';
import { mockMergeRequests } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -51,7 +51,7 @@ describe('MergeRequestTable component', () => {
.at(1);
const updateMergeRequests = (index, attrs) =>
mergeRequests.map((item, idx) => {
mockMergeRequests.map((item, idx) => {
if (idx !== index) {
return item;
}
......@@ -66,7 +66,7 @@ describe('MergeRequestTable component', () => {
beforeEach(() => {
jest.spyOn(global, 'Date').mockImplementationOnce(() => new Date('2020-03-09T11:01:58.135Z'));
bootstrap({ mergeRequests });
bootstrap({ mergeRequests: mockMergeRequests });
});
it('renders the GlTable component', () => {
......
const mergeRequests = [
export const mockMergeRequests = [
{
title:
'This is just a super long merge request title that does not fit into one line so it needs to be truncated',
......@@ -34,4 +34,40 @@ const mergeRequests = [
},
];
export default mergeRequests;
export const mockMilestones = [
{
id: 41,
title: 'Sprint - Eligendi et aut pariatur ab rerum vel.',
project_id: 1,
description: 'Accusamus qui sapiente porro et in voluptates.',
due_date: '2020-01-14',
created_at: '2020-01-08T15:47:37.697Z',
updated_at: '2020-01-08T15:47:37.697Z',
state: 'active',
iid: 6,
start_date: '2020-01-08',
group_id: null,
name: 'Sprint - Eligendi et aut pariatur ab rerum vel.',
},
{
id: 5,
title: 'v4.0',
project_id: 1,
description: 'Atque laudantium reiciendis consequatur temporibus qui qui.',
due_date: null,
created_at: '2020-01-18T15:46:07.448Z',
updated_at: '2020-01-18T15:46:07.448Z',
state: 'active',
iid: 5,
start_date: null,
group_id: null,
name: 'v4.0',
},
];
export const mockLabels = [
[
{ id: 74, title: 'Alero', color: '#6235f2', text_color: '#FFFFFF' },
{ id: 9, title: 'Amsche', color: '#581cc8', text_color: '#FFFFFF' },
],
];
......@@ -4,6 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/analytics/code_review_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/code_review_analytics/store/modules/filters/mutation_types';
import getInitialState from 'ee/analytics/code_review_analytics/store/modules/filters/state';
import createFlash from '~/flash';
import { mockMilestones, mockLabels } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn());
describe('Code review analytics filters actions', () => {
let state;
......@@ -18,19 +22,145 @@ describe('Code review analytics filters actions', () => {
mock.restore();
});
describe('setMilestonesEndpoint', () => {
it('commits the SET_MILESTONES_ENDPOINT mutation', () =>
testAction(
actions.setMilestonesEndpoint,
'milestone_path',
state,
[
{
type: types.SET_MILESTONES_ENDPOINT,
payload: 'milestone_path',
},
],
[],
));
});
describe('setLabelsEndpoint', () => {
it('commits the SET_LABELS_ENDPOINT mutation', () =>
testAction(
actions.setLabelsEndpoint,
'labels_path',
state,
[
{
type: types.SET_LABELS_ENDPOINT,
payload: 'labels_path',
},
],
[],
));
});
describe('fetchMilestones', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(state.milestonesEndpoint).replyOnce(200, mockMilestones);
});
it('dispatches success with received data', () => {
testAction(
actions.fetchMilestones,
null,
state,
[
{ type: types.REQUEST_MILESTONES },
{ type: types.RECEIVE_MILESTONES_SUCCESS, payload: mockMilestones },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(state.milestonesEndpoint).replyOnce(500);
});
it('dispatches error', done => {
testAction(
actions.fetchMilestones,
null,
state,
[
{ type: types.REQUEST_MILESTONES },
{
type: types.RECEIVE_MILESTONES_ERROR,
payload: 500,
},
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
});
describe('fetchLabels', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(state.labelsEndpoint).replyOnce(200, mockLabels);
});
it('dispatches success with received data', () => {
testAction(
actions.fetchLabels,
null,
state,
[
{ type: types.REQUEST_LABELS },
{ type: types.RECEIVE_LABELS_SUCCESS, payload: mockLabels },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(state.labelsEndpoint).replyOnce(500);
});
it('dispatches error', done => {
testAction(
actions.fetchLabels,
null,
state,
[
{ type: types.REQUEST_LABELS },
{
type: types.RECEIVE_LABELS_ERROR,
payload: 500,
},
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
});
describe('setFilters', () => {
const milestoneTitle = 'my milestone';
const labelName = ['first label', 'second label'];
const selectedMilestone = 'my milestone';
const selectedLabels = ['first label', 'second label'];
it('commits the SET_FILTERS mutation', () => {
testAction(
actions.setFilters,
{ milestone_title: milestoneTitle, label_name: labelName },
{ milestone_title: selectedMilestone, label_name: selectedLabels },
state,
[
{
type: types.SET_FILTERS,
payload: { milestoneTitle, labelName },
payload: { selectedMilestone, selectedLabels },
},
],
[
......
import * as types from 'ee/analytics/code_review_analytics/store/modules/filters/mutation_types';
import mutations from 'ee/analytics/code_review_analytics/store/modules/filters/mutations';
import getInitialState from 'ee/analytics/code_review_analytics/store/modules/filters/state';
import { mockMilestones, mockLabels } from '../../../mock_data';
describe('Code review analytics filters mutations', () => {
let state;
......@@ -12,12 +13,109 @@ describe('Code review analytics filters mutations', () => {
state = getInitialState();
});
describe(types.SET_MILESTONES_ENDPOINT, () => {
it('sets the milestone path', () => {
mutations[types.SET_MILESTONES_ENDPOINT](state, 'milestone_path');
expect(state.milestonesEndpoint).toEqual('milestone_path');
});
});
describe(types.SET_LABELS_ENDPOINT, () => {
it('sets the labels path', () => {
mutations[types.SET_LABELS_ENDPOINT](state, 'labels_path');
expect(state.labelsEndpoint).toEqual('labels_path');
});
});
describe(types.REQUEST_MILESTONES, () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_MILESTONES](state);
expect(state.milestones.isLoading).toBe(true);
});
});
describe(types.RECEIVE_MILESTONES_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_MILESTONES_SUCCESS](state, mockMilestones);
});
it.each`
stateProp | value
${'isLoading'} | ${false}
${'errorCode'} | ${null}
${'data'} | ${mockMilestones}
`('sets $stateProp to $value', ({ stateProp, value }) => {
expect(state.milestones[stateProp]).toEqual(value);
});
});
describe(types.RECEIVE_MILESTONES_ERROR, () => {
const errorCode = 500;
beforeEach(() => {
mutations[types.RECEIVE_MILESTONES_ERROR](state, errorCode);
});
it.each`
stateProp | value
${'isLoading'} | ${false}
${'errorCode'} | ${errorCode}
${'data'} | ${[]}
`('sets $stateProp to $value', ({ stateProp, value }) => {
expect(state.milestones[stateProp]).toEqual(value);
});
});
describe(types.REQUEST_LABELS, () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_LABELS](state);
expect(state.labels.isLoading).toBe(true);
});
});
describe(types.RECEIVE_LABELS_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_LABELS_SUCCESS](state, mockLabels);
});
it.each`
stateProp | value
${'isLoading'} | ${false}
${'errorCode'} | ${null}
${'data'} | ${mockLabels}
`('sets $stateProp to $value', ({ stateProp, value }) => {
expect(state.labels[stateProp]).toEqual(value);
});
});
describe(types.RECEIVE_LABELS_ERROR, () => {
const errorCode = 500;
beforeEach(() => {
mutations[types.RECEIVE_LABELS_ERROR](state, errorCode);
});
it.each`
stateProp | value
${'isLoading'} | ${false}
${'errorCode'} | ${errorCode}
${'data'} | ${[]}
`('sets $stateProp to $value', ({ stateProp, value }) => {
expect(state.labels[stateProp]).toEqual(value);
});
});
describe(types.SET_FILTERS, () => {
it('updates milestoneTitle and labelName', () => {
mutations[types.SET_FILTERS](state, { milestoneTitle, labelName });
it('updates selected milestone and labels', () => {
mutations[types.SET_FILTERS](state, {
selectedMilestone: milestoneTitle,
selectedLabels: labelName,
});
expect(state.milestoneTitle).toBe(milestoneTitle);
expect(state.labelName).toBe(labelName);
expect(state.milestones.selected).toBe(milestoneTitle);
expect(state.labels.selected).toBe(labelName);
});
});
});
......@@ -5,7 +5,7 @@ import * as actions from 'ee/analytics/code_review_analytics/store/modules/merge
import * as types from 'ee/analytics/code_review_analytics/store/modules/merge_requests/mutation_types';
import getInitialState from 'ee/analytics/code_review_analytics/store/modules/merge_requests/state';
import createFlash from '~/flash';
import mockMergeRequests from '../../../mock_data';
import { mockMergeRequests } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn());
......@@ -34,8 +34,8 @@ describe('Code review analytics mergeRequests actions', () => {
beforeEach(() => {
state = {
filters: {
milestoneTitle: null,
labelName: [],
milestones: { selected: null },
labels: { selected: [] },
},
...getInitialState(),
};
......
import * as types from 'ee/analytics/code_review_analytics/store/modules/merge_requests/mutation_types';
import mutations from 'ee/analytics/code_review_analytics/store/modules/merge_requests/mutations';
import getInitialState from 'ee/analytics/code_review_analytics/store/modules/merge_requests/state';
import mockMergeRequests from '../../../mock_data';
import * as types from 'ee/analytics/code_review_analytics/store/modules/filters/mutation_types';
import mutations from 'ee/analytics/code_review_analytics/store/modules/filters/mutations';
import getInitialState from 'ee/analytics/code_review_analytics/store/modules/filters/state';
import { mockMilestones } from '../../../mock_data';
describe('Code review analytics mergeRequests mutations', () => {
describe('Code review analytics filters mutations', () => {
let state;
const pageInfo = {
page: 1,
nextPage: 2,
previousPage: 1,
perPage: 10,
total: 50,
totalPages: 5,
};
const milestoneTitle = 'my milestone';
const labelName = ['first label', 'second label'];
beforeEach(() => {
state = getInitialState();
});
describe(types.SET_PROJECT_ID, () => {
it('sets the project id', () => {
mutations[types.SET_PROJECT_ID](state, 1);
describe(types.SET_MILESTONES_ENDPOINT, () => {
it('sets the milestone path', () => {
mutations[types.SET_MILESTONES_ENDPOINT](state, 'milestone_path');
expect(state.projectId).toBe(1);
expect(state.milestonesEndpoint).toEqual('milestone_path');
});
});
describe(types.REQUEST_MERGE_REQUESTS, () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_MERGE_REQUESTS](state);
describe(types.SET_LABELS_ENDPOINT, () => {
it('sets the labels path', () => {
mutations[types.SET_LABELS_ENDPOINT](state, 'labels_path');
expect(state.isLoading).toBe(true);
expect(state.labelsEndpoint).toEqual('labels_path');
});
});
describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
it('updates mergeRequests with the received data and updates the pageInfo', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, {
pageInfo,
mergeRequests: mockMergeRequests,
describe(types.REQUEST_MILESTONES, () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_MILESTONES](state);
expect(state.milestones.isLoading).toBe(true);
});
});
describe(types.RECEIVE_MILESTONES_SUCCESS, () => {
it('updates milestone data with the received data', () => {
mutations[types.RECEIVE_MILESTONES_SUCCESS](state, mockMilestones);
expect(state.isLoading).toBe(false);
expect(state.errorCode).toBe(null);
expect(state.mergeRequests).toEqual(mockMergeRequests);
expect(state.pageInfo).toEqual(pageInfo);
expect(state.milestones.isLoading).toBe(false);
expect(state.milestones.errorCode).toBe(null);
expect(state.milestones.data).toEqual(mockMilestones);
});
});
describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
describe(types.RECEIVE_MILESTONES_ERROR, () => {
const errorCode = 500;
beforeEach(() => {
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](state, errorCode);
mutations[types.RECEIVE_MILESTONES_ERROR](state, errorCode);
});
it('sets isLoading to false', () => {
expect(state.isLoading).toBe(false);
expect(state.milestones.isLoading).toBe(false);
});
it('sets errorCode to 500', () => {
expect(state.errorCode).toBe(errorCode);
expect(state.milestones.errorCode).toBe(errorCode);
});
it('clears data', () => {
expect(state.mergeRequests).toEqual([]);
expect(state.pageInfo).toEqual({});
expect(state.milestones.data).toEqual([]);
});
});
describe('SET_PAGE', () => {
it('sets the page on the pageInfo object', () => {
mutations[types.SET_PAGE](state, 2);
describe(types.SET_FILTERS, () => {
it('updates selected milestone and labels', () => {
mutations[types.SET_FILTERS](state, {
selectedMilestone: milestoneTitle,
selectedLabels: labelName,
});
expect(state.pageInfo.page).toBe(2);
expect(state.milestones.selected).toBe(milestoneTitle);
expect(state.labels.selected).toBe(labelName);
});
});
});
......@@ -8834,6 +8834,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
msgid "Failed to load labels. Please try again."
msgstr ""
msgid "Failed to load milestones. Please try again."
msgstr ""
msgid "Failed to load related branches"
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