Commit 90c74431 authored by Michael Lunøe's avatar Michael Lunøe Committed by Kushal Pandya

Refactor(Analytics): introduce store filter module

Use the common store filter module for both
Code Review Analytics filter bar
Value Stream Analytics filter bar

This streamlines the filter bar store module and
prepares it to be used for the rest of analytics
parent 77c218a4
/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
export const ANY_AUTHOR = 'Any';
export const NO_LABEL = 'No label';
const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEBOUNCE_DELAY = 200;
......@@ -11,13 +16,11 @@ export const SortDirection = {
ascending: 'ascending',
};
export const defaultMilestones = [
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'None', text: __('None') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Any', text: __('Any') },
// eslint-disable-next-line @gitlab/require-i18n-strings
export const DEFAULT_MILESTONES = [
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
{ value: 'Upcoming', text: __('Upcoming') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Started', text: __('Started') },
];
/* eslint-enable @gitlab/require-i18n-strings */
......@@ -14,10 +14,9 @@ import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { stripQuotes } from '../filtered_search_utils';
import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
export default {
noLabel: NO_LABEL,
components: {
GlToken,
GlFilteredSearchToken,
......@@ -38,6 +37,7 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: true,
};
},
......@@ -105,9 +105,13 @@ export default {
>
</template>
<template #suggestions>
<gl-filtered-search-suggestion :value="$options.noLabel">{{
__('No label')
}}</gl-filtered-search-suggestion>
<gl-filtered-search-suggestion
v-for="label in defaultLabels"
:key="label.value"
:value="label.value"
>
{{ label.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
<gl-loading-icon v-if="loading" />
<template v-else>
......
......@@ -11,10 +11,9 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import { stripQuotes } from '../filtered_search_utils';
import { defaultMilestones, DEBOUNCE_DELAY } from '../constants';
import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
export default {
defaultMilestones,
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
......@@ -34,6 +33,7 @@ export default {
data() {
return {
milestones: this.config.initialMilestones || [],
defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: true,
};
},
......@@ -89,11 +89,12 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="milestone in $options.defaultMilestones"
v-for="milestone in defaultMilestones"
:key="milestone.value"
:value="milestone.value"
>{{ milestone.text }}</gl-filtered-search-suggestion
>
{{ milestone.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
<gl-loading-icon v-if="loading" />
<template v-else>
......
......@@ -67,15 +67,17 @@ export default {
this.filterManager = new FilteredSearchCodeReviewAnalytics();
this.filterManager.setup();
} else {
this.setMilestonesEndpoint(this.milestonePath);
this.setLabelsEndpoint(this.labelsPath);
this.setEndpoints({
milestonesEndpoint: this.milestonePath,
labelsEndpoint: this.labelsPath,
});
}
this.setProjectId(this.projectId);
this.fetchMergeRequests();
},
methods: {
...mapActions('filters', ['setMilestonesEndpoint', 'setLabelsEndpoint']),
...mapActions('filters', ['setEndpoints']),
...mapActions('mergeRequests', ['setProjectId', 'fetchMergeRequests', 'setPage']),
},
};
......
......@@ -4,6 +4,7 @@ import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filte
import { __ } from '~/locale';
import MilestoneToken from '../../shared/components/tokens/milestone_token.vue';
import LabelToken from '../../shared/components/tokens/label_token.vue';
import { processFilters } from '../../shared/utils';
export default {
components: {
......@@ -45,7 +46,7 @@ export default {
{
icon: 'labels',
title: __('Label'),
type: 'label',
type: 'labels',
token: LabelToken,
labels: this.labels,
unique: false,
......@@ -62,32 +63,13 @@ export default {
},
methods: {
...mapActions('filters', ['fetchMilestones', 'fetchLabels', 'setFilters']),
processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
let tokenValue = value.data;
// remove wrapping double quotes which were added for token values that include spaces
if (
(tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
) {
tokenValue = tokenValue.slice(1, -1);
}
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
},
handleFilter(filters) {
const { label: labelNames, milestone } = this.processFilters(filters);
const milestoneTitle = milestone ? milestone[0] : null;
this.setFilters({ labelNames, milestoneTitle });
const { labels, milestone } = processFilters(filters);
this.setFilters({
selectedMilestone: milestone ? milestone[0] : null,
selectedLabels: labels,
});
},
},
};
......
export function setFilters({ dispatch }) {
return Promise.all([
dispatch('mergeRequests/setPage', 1),
dispatch('mergeRequests/fetchMergeRequests', null),
]);
}
import Vue from 'vue';
import Vuex from 'vuex';
import filters from './modules/filters/index';
import filters from 'ee/analytics/shared/store/modules/filters';
import * as actions from './actions';
import mergeRequests from './modules/merge_requests/index';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
modules: {
filters,
mergeRequests,
......
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
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 }, search_title = '') => {
commit(types.REQUEST_MILESTONES);
return axios
.get(state.milestonesEndpoint, { params: { search_title } })
.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 }, search = '') => {
commit(types.REQUEST_LABELS);
return axios
.get(state.labelsEndpoint, { params: { search } })
.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 }, { labelNames, milestoneTitle }) => {
commit(types.SET_FILTERS, {
selectedLabels: labelNames,
selectedMilestone: milestoneTitle,
});
dispatch('mergeRequests/setPage', 1, { root: true });
dispatch('mergeRequests/fetchMergeRequests', null, { root: true });
};
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_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 () => ({
milestonePath: '',
labelsPath: '',
milestones: {
isLoading: false,
data: [],
errorCode: null,
selected: null,
},
labels: {
isLoading: false,
data: [],
errorCode: null,
selected: [],
},
});
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
export const prepareTokens = ({
milestone = null,
author = null,
assignees = [],
labels = [],
} = {}) => {
const authorToken = author ? [{ type: 'author', value: { data: author } }] : [];
const milestoneToken = milestone ? [{ type: 'milestone', value: { data: milestone } }] : [];
const assigneeTokens = assignees?.length
? assignees.map(data => ({ type: 'assignees', value: { data } }))
: [];
const labelTokens = labels?.length
? labels.map(data => ({ type: 'labels', value: { data } }))
: [];
return [...authorToken, ...milestoneToken, ...assigneeTokens, ...labelTokens];
};
import UrlSync from '~/vue_shared/components/url_sync.vue';
import {
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { prepareTokens, processFilters } from '../../shared/utils';
export default {
name: 'FilterBar',
......@@ -66,6 +53,7 @@ export default {
title: __('Label'),
type: 'labels',
token: LabelToken,
defaultLabels: [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY],
initialLabels: this.labelsData,
unique: false,
symbol: '~',
......@@ -123,30 +111,8 @@ export default {
} = this;
return prepareTokens({ milestone, author, assignees, labels });
},
processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
let tokenValue = value.data;
// remove wrapping double quotes which were added for token values that include spaces
if (
(tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
) {
tokenValue = tokenValue.slice(1, -1);
}
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
},
handleFilter(filters) {
const { labels, milestone, author, assignees } = this.processFilters(filters);
const { labels, milestone, author, assignees } = processFilters(filters);
this.setFilters({
selectedAuthor: author ? author[0].value : null,
......
......@@ -8,15 +8,17 @@ import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../ut
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ dispatch }, options) => {
const { groupPath = '', milestonesPath = '', labelsPath = '' } = options;
const { group, milestonesPath = '', labelsPath = '' } = options;
// TODO: After we remove instance VSA we can rely on the paths from the BE
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
const groupPath = group?.parentId || group?.fullPath || '';
const milestonesEndpoint = milestonesPath || `/groups/${groupPath}/-/milestones`;
const labelsEndpoint = labelsPath || `/groups/${groupPath}/-/labels`;
return dispatch('filters/setEndpoints', {
labelsEndpoint: appendExtension(labelsEndpoint),
milestonesEndpoint: appendExtension(milestonesEndpoint),
groupEndpoint: groupPath,
});
};
......@@ -271,7 +273,7 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
if (initialData.group?.fullPath) {
return Promise.all([
dispatch('setPaths', { groupPath: initialData.group.fullPath, milestonesPath, labelsPath }),
dispatch('setPaths', { group: initialData.group, milestonesPath, labelsPath }),
dispatch('filters/initialize', {
selectedAuthor,
selectedMilestone,
......
......@@ -12,9 +12,6 @@ export const currentValueStreamId = ({ selectedValueStream }) =>
export const currentGroupPath = ({ selectedGroup }) => selectedGroup?.fullPath || null;
export const currentGroupParentPath = ({ selectedGroup }, getters) =>
selectedGroup?.parentId || getters.currentGroupPath;
export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects?.map(({ id }) => id) || [];
......
import Vue from 'vue';
import Vuex from 'vuex';
import filters from 'ee/analytics/shared/store/modules/filters';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
......@@ -7,7 +8,6 @@ import state from './state';
import customStages from './modules/custom_stages/index';
import durationChart from './modules/duration_chart/index';
import typeOfWork from './modules/type_of_work/index';
import filters from './modules/filters/index';
Vue.use(Vuex);
......
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
export default {
namespaced: true,
actions,
mutations,
state: state(),
};
......@@ -4,9 +4,10 @@ import { __ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types';
export const setEndpoints = ({ commit }, { milestonesEndpoint, labelsEndpoint }) => {
export const setEndpoints = ({ commit }, { milestonesEndpoint, labelsEndpoint, groupEndpoint }) => {
commit(types.SET_MILESTONES_ENDPOINT, milestonesEndpoint);
commit(types.SET_LABELS_ENDPOINT, labelsEndpoint);
commit(types.SET_GROUP_ENDPOINT, groupEndpoint);
};
export const fetchMilestones = ({ commit, state }, search_title = '') => {
......@@ -57,23 +58,23 @@ const fetchUser = ({ commit, endpoint, query, action, errorMessage }) => {
});
};
export const fetchAuthors = ({ commit, rootGetters }, query = '') => {
const { currentGroupParentPath } = rootGetters;
export const fetchAuthors = ({ commit, state }, query = '') => {
const { groupEndpoint } = state;
return fetchUser({
commit,
query,
endpoint: currentGroupParentPath,
endpoint: groupEndpoint,
action: 'AUTHORS',
errorMessage: __('Failed to load authors. Please try again.'),
});
};
export const fetchAssignees = ({ commit, rootGetters }, query = '') => {
const { currentGroupParentPath } = rootGetters;
export const fetchAssignees = ({ commit, state }, query = '') => {
const { groupEndpoint } = state;
return fetchUser({
commit,
query,
endpoint: currentGroupParentPath,
endpoint: groupEndpoint,
action: 'ASSIGNEES',
errorMessage: __('Failed to load assignees. Please try again.'),
});
......
export const SET_MILESTONES_ENDPOINT = 'SET_MILESTONES_ENDPOINT';
export const SET_LABELS_ENDPOINT = 'SET_LABELS_ENDPOINT';
export const SET_GROUP_ENDPOINT = 'SET_GROUP_ENDPOINT';
export const REQUEST_MILESTONES = 'REQUEST_MILESTONES';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
......
......@@ -19,15 +19,20 @@ export default {
[types.SET_LABELS_ENDPOINT](state, labelsEndpoint) {
state.labelsEndpoint = labelsEndpoint;
},
[types.SET_GROUP_ENDPOINT](state, groupEndpoint) {
state.groupEndpoint = groupEndpoint;
},
[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) {
[types.RECEIVE_MILESTONES_ERROR](state, errorCode) {
state.milestones.isLoading = false;
state.milestones.errorCode = errorCode;
state.milestones.data = [];
},
[types.REQUEST_LABELS](state) {
......@@ -36,9 +41,11 @@ export default {
[types.RECEIVE_LABELS_SUCCESS](state, data) {
state.labels.isLoading = false;
state.labels.data = data;
state.labels.errorCode = null;
},
[types.RECEIVE_LABELS_ERROR](state) {
[types.RECEIVE_LABELS_ERROR](state, errorCode) {
state.labels.isLoading = false;
state.labels.errorCode = errorCode;
state.labels.data = [];
},
[types.REQUEST_AUTHORS](state) {
......@@ -47,9 +54,11 @@ export default {
[types.RECEIVE_AUTHORS_SUCCESS](state, data) {
state.authors.isLoading = false;
state.authors.data = data;
state.authors.errorCode = null;
},
[types.RECEIVE_AUTHORS_ERROR](state) {
[types.RECEIVE_AUTHORS_ERROR](state, errorCode) {
state.authors.isLoading = false;
state.authors.errorCode = errorCode;
state.authors.data = [];
},
[types.REQUEST_ASSIGNEES](state) {
......@@ -58,9 +67,11 @@ export default {
[types.RECEIVE_ASSIGNEES_SUCCESS](state, data) {
state.assignees.isLoading = false;
state.assignees.data = data;
state.assignees.errorCode = null;
},
[types.RECEIVE_ASSIGNEES_ERROR](state) {
[types.RECEIVE_ASSIGNEES_ERROR](state, errorCode) {
state.assignees.isLoading = false;
state.assignees.errorCode = errorCode;
state.assignees.data = [];
},
};
export default () => ({
milestonesEndpoint: '',
labelsEndpoint: '',
groupEndpoint: '',
milestones: {
isLoading: false,
errorCode: null,
data: [],
selected: null,
},
labels: {
isLoading: false,
errorCode: null,
data: [],
selected: [],
},
authors: {
isLoading: false,
errorCode: null,
data: [],
selected: null,
},
assignees: {
isLoading: false,
errorCode: null,
data: [],
selected: [],
},
......
......@@ -114,3 +114,28 @@ export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'na
if (!searchTerm?.length) return data;
return data.filter(item => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
};
export const prepareTokens = (tokens = {}) => {
const { milestone = null, author = null, assignees = [], labels = [] } = tokens;
const authorToken = author ? [{ type: 'author', value: { data: author } }] : [];
const milestoneToken = milestone ? [{ type: 'milestone', value: { data: milestone } }] : [];
const assigneeTokens = assignees?.map(data => ({ type: 'assignees', value: { data } })) || [];
const labelTokens = labels?.map(data => ({ type: 'labels', value: { data } })) || [];
return [...authorToken, ...milestoneToken, ...assigneeTokens, ...labelTokens];
};
export function processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
const tokenValue = value.data;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
}
......@@ -4,8 +4,9 @@ import { GlLoadingIcon, GlEmptyState, GlBadge, GlPagination } from '@gitlab/ui';
import CodeReviewAnalyticsApp from 'ee/analytics/code_review_analytics/components/app.vue';
import MergeRequestTable from 'ee/analytics/code_review_analytics/components/merge_request_table.vue';
import FilterBar from 'ee/analytics/code_review_analytics/components/filter_bar.vue';
import * as actions from 'ee/analytics/code_review_analytics/store/actions';
import createMergeRequestsState from 'ee/analytics/code_review_analytics/store/modules/merge_requests/state';
import createFiltersState from 'ee/analytics/code_review_analytics/store/modules/filters/state';
import createFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import { TEST_HOST } from 'helpers/test_constants';
const mockFilterManagerSetup = jest.fn();
......@@ -24,8 +25,7 @@ describe('CodeReviewAnalyticsApp component', () => {
let setPage;
let fetchMergeRequests;
let setMilestonesEndpoint;
let setLabelsEndpoint;
let setEndpoints;
const pageInfo = {
page: 1,
......@@ -35,6 +35,7 @@ describe('CodeReviewAnalyticsApp component', () => {
const createStore = (initialState = {}, getters = {}) =>
new Vuex.Store({
actions,
modules: {
mergeRequests: {
namespaced: true,
......@@ -59,8 +60,7 @@ describe('CodeReviewAnalyticsApp component', () => {
...initialState.filters,
},
actions: {
setMilestonesEndpoint,
setLabelsEndpoint,
setEndpoints,
},
},
},
......@@ -88,8 +88,7 @@ describe('CodeReviewAnalyticsApp component', () => {
beforeEach(() => {
setPage = jest.fn();
fetchMergeRequests = jest.fn();
setMilestonesEndpoint = jest.fn();
setLabelsEndpoint = jest.fn();
setEndpoints = jest.fn();
});
afterEach(() => {
......@@ -118,12 +117,8 @@ describe('CodeReviewAnalyticsApp component', () => {
expect(mockFilterManagerSetup).toHaveBeenCalled();
});
it('does not call setMilestonesEndpoint action', () => {
expect(setMilestonesEndpoint).not.toHaveBeenCalled();
});
it('does not call setLabelsEndpoint action', () => {
expect(setLabelsEndpoint).not.toHaveBeenCalled();
it('does not call setEndpoints action', () => {
expect(setEndpoints).not.toHaveBeenCalled();
});
});
......@@ -142,12 +137,8 @@ describe('CodeReviewAnalyticsApp component', () => {
expect(mockFilterManagerSetup).not.toHaveBeenCalled();
});
it('calls setMilestonesEndpoint action', () => {
expect(setMilestonesEndpoint).toHaveBeenCalled();
});
it('calls setLabelsEndpoint action', () => {
expect(setLabelsEndpoint).toHaveBeenCalled();
it('calls setEndpoints action', () => {
expect(setEndpoints).toHaveBeenCalled();
});
});
});
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import * as utils from 'ee/analytics/shared/utils';
import FilterBar from 'ee/analytics/code_review_analytics/components/filter_bar.vue';
import createFiltersState from 'ee/analytics/code_review_analytics/store/modules/filters/state';
import createFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { mockMilestones, mockLabels } from '../mock_data';
......@@ -9,7 +10,7 @@ const localVue = createLocalVue();
localVue.use(Vuex);
const milestoneTokenType = 'milestone';
const labelTokenType = 'label';
const labelTokenType = 'labels';
describe('FilteredSearchBar', () => {
let wrapper;
......@@ -101,64 +102,24 @@ describe('FilteredSearchBar', () => {
labels: { data: mockLabels },
});
wrapper = createComponent(vuexStore);
jest.spyOn(utils, 'processFilters');
});
it('clicks on the search button, setFilters is dispatched', () => {
findFilteredSearch().vm.$emit('onFilter', [
const filters = [
{ type: 'milestone', value: { data: 'my-milestone', operator: '=' } },
{ type: 'label', value: { data: 'my-label', operator: '=' } },
]);
{ type: 'labels', value: { data: 'my-label', operator: '=' } },
];
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
labelNames: [{ value: 'my-label', operator: '=' }],
milestoneTitle: { value: 'my-milestone', operator: '=' },
},
undefined,
);
});
it('removes wrapping double quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
labelNames: undefined,
milestoneTitle: { value: 'milestone with spaces', operator: '=' },
},
undefined,
);
});
it('removes wrapping single quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: "'milestone with spaces'", operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
labelNames: undefined,
milestoneTitle: { value: 'milestone with spaces', operator: '=' },
},
undefined,
);
});
findFilteredSearch().vm.$emit('onFilter', filters);
it('does not remove inner double quotes from the data and dispatches setFilters ', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: 'milestone "with" spaces', operator: '=' } },
]);
expect(utils.processFilters).toHaveBeenCalledWith(filters);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
labelNames: undefined,
milestoneTitle: { value: 'milestone "with" spaces', operator: '=' },
selectedLabels: [{ value: 'my-label', operator: '=' }],
selectedMilestone: { value: 'my-milestone', operator: '=' },
},
undefined,
);
......
import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/analytics/code_review_analytics/store/actions';
describe('Code review analytics actions', () => {
let state;
describe('setFilters', () => {
const selectedMilestone = { value: 'my milestone', operator: '=' };
const selectedLabels = [
{ value: 'first label', operator: '=' },
{ value: 'second label', operator: '!=' },
];
it('commits the SET_FILTERS mutation', () => {
testAction(
actions.setFilters,
{ labelNames: selectedLabels, milestoneTitle: selectedMilestone },
state,
[],
[
{ type: 'mergeRequests/setPage', payload: 1 },
{ type: 'mergeRequests/fetchMergeRequests', payload: null },
],
);
});
});
});
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
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 { deprecatedCreateFlash as createFlash } from '~/flash';
import { mockMilestones, mockLabels } from '../../../mock_data';
jest.mock('~/flash');
describe('Code review analytics filters actions', () => {
let state;
let mock;
beforeEach(() => {
state = getInitialState();
mock = new MockAdapter(axios);
});
afterEach(() => {
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 selectedMilestone = { value: 'my milestone', operator: '=' };
const selectedLabels = [
{ value: 'first label', operator: '=' },
{ value: 'second label', operator: '!=' },
];
it('commits the SET_FILTERS mutation', () => {
testAction(
actions.setFilters,
{ labelNames: selectedLabels, milestoneTitle: selectedMilestone },
state,
[
{
type: types.SET_FILTERS,
payload: { selectedMilestone, selectedLabels },
},
],
[
{ type: 'mergeRequests/setPage', payload: 1 },
{ type: 'mergeRequests/fetchMergeRequests', payload: null },
],
);
});
});
});
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;
const milestoneTitle = 'my milestone';
const labelName = ['first label', 'second label'];
beforeEach(() => {
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 selected milestone and labels', () => {
mutations[types.SET_FILTERS](state, {
selectedMilestone: milestoneTitle,
selectedLabels: labelName,
});
expect(state.milestones.selected).toBe(milestoneTitle);
expect(state.labels.selected).toBe(labelName);
});
});
});
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 filters mutations', () => {
let state;
const milestoneTitle = 'my milestone';
const labelName = ['first label', 'second label'];
beforeEach(() => {
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, () => {
it('updates milestone data with the received data', () => {
mutations[types.RECEIVE_MILESTONES_SUCCESS](state, mockMilestones);
expect(state.milestones.isLoading).toBe(false);
expect(state.milestones.errorCode).toBe(null);
expect(state.milestones.data).toEqual(mockMilestones);
});
});
describe(types.RECEIVE_MILESTONES_ERROR, () => {
const errorCode = 500;
beforeEach(() => {
mutations[types.RECEIVE_MILESTONES_ERROR](state, errorCode);
});
it('sets isLoading to false', () => {
expect(state.milestones.isLoading).toBe(false);
});
it('sets errorCode to 500', () => {
expect(state.milestones.errorCode).toBe(errorCode);
});
it('clears data', () => {
expect(state.milestones.data).toEqual([]);
});
});
describe(types.SET_FILTERS, () => {
it('updates selected milestone and labels', () => {
mutations[types.SET_FILTERS](state, {
selectedMilestone: milestoneTitle,
selectedLabels: labelName,
});
expect(state.milestones.selected).toBe(milestoneTitle);
expect(state.labels.selected).toBe(labelName);
});
});
});
......@@ -2,12 +2,13 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as utils from 'ee/analytics/shared/utils';
import storeConfig from 'ee/analytics/cycle_analytics/store';
import FilterBar, { prepareTokens } from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { filterMilestones, filterLabels } from '../mock_data';
import { filterMilestones, filterLabels } from '../../shared/store/modules/filters/mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
......@@ -150,101 +151,32 @@ describe('Filter bar', () => {
labels: { data: filterLabels },
});
wrapper = createComponent(store);
jest.spyOn(utils, 'processFilters');
});
it('clicks on the search button, setFilters is dispatched', () => {
findFilteredSearch().vm.$emit('onFilter', [
const filters = [
{ type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
{ type: 'labels', value: { data: selectedLabels[0].title, operator: '=' } },
]);
];
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedLabels: [selectedLabels[0].title],
selectedMilestone: selectedMilestone[0].title,
selectedAssignees: [],
selectedAuthor: null,
},
undefined,
);
});
findFilteredSearch().vm.$emit('onFilter', filters);
it('removes wrapping double quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
expect(utils.processFilters).toHaveBeenCalledWith(filters);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedMilestone: 'milestone with spaces',
selectedLabels: [],
selectedAssignees: [],
selectedAuthor: null,
},
undefined,
);
});
it('removes wrapping single quotes from the data and dispatches setFilters', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: "'milestone with spaces'", operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedMilestone: 'milestone with spaces',
selectedLabels: [],
selectedAssignees: [],
selectedAuthor: null,
},
undefined,
);
});
it('does not remove inner double quotes from the data and dispatches setFilters ', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: 'milestone "with" spaces', operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedMilestone: 'milestone "with" spaces',
selectedLabels: [selectedLabels[0].title],
selectedMilestone: selectedMilestone[0].title,
selectedAssignees: [],
selectedAuthor: null,
selectedLabels: [],
},
undefined,
);
});
});
describe('prepareTokens', () => {
describe('with empty data', () => {
it('returns an empty array', () => {
expect(prepareTokens()).toEqual([]);
expect(prepareTokens({})).toEqual([]);
expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
[],
);
});
});
it.each`
token | value | result
${'milestone'} | ${'v1.0'} | ${[{ type: 'milestone', value: { data: 'v1.0' } }]}
${'author'} | ${'mr.popo'} | ${[{ type: 'author', value: { data: 'mr.popo' } }]}
${'labels'} | ${['z-fighters']} | ${[{ type: 'labels', value: { data: 'z-fighters' } }]}
${'assignees'} | ${['krillin', 'piccolo']} | ${[{ type: 'assignees', value: { data: 'krillin' } }, { type: 'assignees', value: { data: 'piccolo' } }]}
`('with $token=$value sets the $token key', ({ token, value, result }) => {
const res = prepareTokens({ [token]: value });
expect(res).toEqual(result);
});
});
describe.each`
stateKey | payload | paramKey
${'selectedMilestone'} | ${'12.0'} | ${'milestone_title'}
......
......@@ -269,54 +269,3 @@ export const selectedProjects = [
// Value returned from JSON fixture is 345600 for issue stage which equals 4d
export const pathNavIssueMetric = '4d';
export const filterMilestones = [
{ id: 1, title: 'None', name: 'Any' },
{ id: 101, title: 'Any', name: 'None' },
{ id: 1001, title: 'v1.0', name: 'v1.0' },
{ id: 10101, title: 'v0.0', name: 'v0.0' },
];
export const filterUsers = [
{
id: 31,
name: 'VSM User2',
username: 'vsm-user-2-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/762398957a8c6e04eed16da88098899d?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-2-1589776313',
access_level: 30,
expires_at: null,
},
{
id: 32,
name: 'VSM User3',
username: 'vsm-user-3-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/f78932237e8a5c5376b65a709824802f?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-3-1589776313',
access_level: 30,
expires_at: null,
},
{
id: 33,
name: 'VSM User4',
username: 'vsm-user-4-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/ab506dc600d1a941e4d77d5ceeeba73f?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-4-1589776313',
access_level: 30,
expires_at: null,
},
];
export const filterLabels = [
{ id: 194, title: 'Afterfunc-Phureforge-781', color: '#990000', text_color: '#FFFFFF' },
{ id: 10, title: 'Afternix', color: '#16ecf2', text_color: '#FFFFFF' },
{ id: 176, title: 'Panasync-Pens-266', color: '#990000', text_color: '#FFFFFF' },
{ id: 79, title: 'Passat', color: '#f1a3d4', text_color: '#333333' },
{ id: 197, title: 'Phast-Onesync-395', color: '#990000', text_color: '#FFFFFF' },
];
......@@ -16,7 +16,7 @@ import {
valueStreams,
} from '../mock_data';
const groupPath = 'fake_group_path';
const group = { parentId: 'fake_group_parent_id', fullPath: 'fake_group_full_path' };
const milestonesPath = 'fake_milestones_path';
const labelsPath = 'fake_labels_path';
......@@ -107,16 +107,17 @@ describe('Cycle analytics actions', () => {
describe('setPaths', () => {
describe('with endpoint paths provided', () => {
it('dispatches the filters/setEndpoints action', () => {
it('dispatches the filters/setEndpoints action with enpoints', () => {
return testAction(
actions.setPaths,
{ groupPath, milestonesPath, labelsPath },
{ group, milestonesPath, labelsPath },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: 'fake_group_parent_id',
labelsEndpoint: 'fake_labels_path.json',
milestonesEndpoint: 'fake_milestones_path.json',
},
......@@ -127,23 +128,66 @@ describe('Cycle analytics actions', () => {
});
describe('without endpoint paths provided', () => {
it('dispatches the filters/setEndpoints action', () => {
it('dispatches the filters/setEndpoints action and prefers group.parentId', () => {
return testAction(
actions.setPaths,
{ groupPath },
{ group },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
labelsEndpoint: '/groups/fake_group_path/-/labels.json',
milestonesEndpoint: '/groups/fake_group_path/-/milestones.json',
groupEndpoint: 'fake_group_parent_id',
labelsEndpoint: '/groups/fake_group_parent_id/-/labels.json',
milestonesEndpoint: '/groups/fake_group_parent_id/-/milestones.json',
},
},
],
);
});
it('dispatches the filters/setEndpoints action and uses group.fullPath', () => {
const { fullPath } = group;
return testAction(
actions.setPaths,
{ group: { fullPath } },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: 'fake_group_full_path',
labelsEndpoint: '/groups/fake_group_full_path/-/labels.json',
milestonesEndpoint: '/groups/fake_group_full_path/-/milestones.json',
},
},
],
);
});
it.each([undefined, null, { parentId: null }, { fullPath: null }, {}])(
'group=%s will return empty string',
value => {
return testAction(
actions.setPaths,
{ group: value, milestonesPath, labelsPath },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: '',
labelsEndpoint: 'fake_labels_path.json',
milestonesEndpoint: 'fake_milestones_path.json',
},
},
],
);
},
);
});
});
......
......@@ -70,42 +70,6 @@ describe('Cycle analytics getters', () => {
});
});
describe('currentGroupParentPath', () => {
const fullPath = 'cool-beans';
const parentId = 'subgroup/parent/id';
describe('with a subgroup', () => {
it('returns the parentId value of the sub group', () => {
state = {
selectedGroup: {
fullPath,
parentId,
},
};
expect(getters.currentGroupParentPath(state)).toEqual(parentId);
});
});
describe('with a parent group', () => {
it('returns the fullPath value of the group', () => {
const res = getters.currentGroupParentPath(
{
selectedGroup: {
fullPath,
},
},
{
...getters,
currentGroupPath: fullPath,
},
);
expect(res).toEqual(fullPath);
});
});
});
describe('cycleAnalyticsRequestParams', () => {
const selectedAuthor = 'Gohan';
const selectedMilestone = 'SSJ4';
......
......@@ -3,6 +3,8 @@ import {
buildProjectFromDataset,
buildCycleAnalyticsInitialData,
filterBySearchTerm,
prepareTokens,
processFilters,
} from 'ee/analytics/shared/utils';
const groupDataset = {
......@@ -233,3 +235,50 @@ describe('buildCycleAnalyticsInitialData', () => {
});
});
});
describe('prepareTokens', () => {
describe('with empty data', () => {
it('returns an empty array', () => {
expect(prepareTokens()).toEqual([]);
expect(prepareTokens({})).toEqual([]);
expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
[],
);
});
});
it.each`
token | value | result
${'milestone'} | ${'v1.0'} | ${[{ type: 'milestone', value: { data: 'v1.0' } }]}
${'author'} | ${'mr.popo'} | ${[{ type: 'author', value: { data: 'mr.popo' } }]}
${'labels'} | ${['z-fighters']} | ${[{ type: 'labels', value: { data: 'z-fighters' } }]}
${'assignees'} | ${['krillin', 'piccolo']} | ${[{ type: 'assignees', value: { data: 'krillin' } }, { type: 'assignees', value: { data: 'piccolo' } }]}
`('with $token=$value sets the $token key', ({ token, value, result }) => {
const res = prepareTokens({ [token]: value });
expect(res).toEqual(result);
});
});
describe('processFilters', () => {
it('processes multiple filter values', () => {
const result = processFilters([
{ type: 'milestone', value: { data: 'my-milestone', operator: '=' } },
{ type: 'labels', value: { data: 'my-label', operator: '=' } },
]);
expect(result).toStrictEqual({
labels: [{ value: 'my-label', operator: '=' }],
milestone: [{ value: 'my-milestone', operator: '=' }],
});
});
it('does not remove wrapping double quotes from the data', () => {
const result = processFilters([
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
expect(result).toStrictEqual({
milestone: [{ value: '"milestone with spaces"', operator: '=' }],
});
});
});
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/analytics/cycle_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types';
import initialState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
import * as actions from 'ee/analytics/shared/store/modules/filters/actions';
import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types';
import initialState from 'ee/analytics/shared/store/modules/filters/state';
import httpStatusCodes from '~/lib/utils/http_status';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { filterMilestones, filterUsers, filterLabels } from '../../../mock_data';
import { filterMilestones, filterUsers, filterLabels } from './mock_data';
const milestonesEndpoint = 'fake_milestones_endpoint';
const labelsEndpoint = 'fake_labels_endpoint';
const groupEndpoint = 'fake_group_endpoint';
jest.mock('~/flash');
......@@ -35,6 +36,7 @@ describe('Filters actions', () => {
const initialData = {
milestonesEndpoint,
labelsEndpoint,
groupEndpoint,
selectedAuthor: 'Mr cool',
selectedMilestone: 'NEXT',
};
......@@ -96,11 +98,12 @@ describe('Filters actions', () => {
it('sets the api paths', () => {
return testAction(
actions.setEndpoints,
{ milestonesEndpoint, labelsEndpoint },
{ milestonesEndpoint, labelsEndpoint, groupEndpoint },
state,
[
{ payload: 'fake_milestones_endpoint', type: types.SET_MILESTONES_ENDPOINT },
{ payload: 'fake_labels_endpoint', type: types.SET_LABELS_ENDPOINT },
{ payload: 'fake_group_endpoint', type: types.SET_GROUP_ENDPOINT },
],
[],
);
......
export const filterMilestones = [
{ id: 1, title: 'None', name: 'Any' },
{ id: 101, title: 'Any', name: 'None' },
{ id: 1001, title: 'v1.0', name: 'v1.0' },
{ id: 10101, title: 'v0.0', name: 'v0.0' },
];
export const filterUsers = [
{
id: 31,
name: 'VSM User2',
username: 'vsm-user-2-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/762398957a8c6e04eed16da88098899d?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-2-1589776313',
access_level: 30,
expires_at: null,
},
{
id: 32,
name: 'VSM User3',
username: 'vsm-user-3-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/f78932237e8a5c5376b65a709824802f?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-3-1589776313',
access_level: 30,
expires_at: null,
},
{
id: 33,
name: 'VSM User4',
username: 'vsm-user-4-1589776313',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/ab506dc600d1a941e4d77d5ceeeba73f?s=80\u0026d=identicon',
web_url: 'http://127.0.0.1:3001/vsm-user-4-1589776313',
access_level: 30,
expires_at: null,
},
];
export const filterLabels = [
{ id: 194, title: 'Afterfunc-Phureforge-781', color: '#990000', text_color: '#FFFFFF' },
{ id: 10, title: 'Afternix', color: '#16ecf2', text_color: '#FFFFFF' },
{ id: 176, title: 'Panasync-Pens-266', color: '#990000', text_color: '#FFFFFF' },
{ id: 79, title: 'Passat', color: '#f1a3d4', text_color: '#333333' },
{ id: 197, title: 'Phast-Onesync-395', color: '#990000', text_color: '#FFFFFF' },
];
import mutations from 'ee/analytics/cycle_analytics/store/modules/filters/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types';
import mutations from 'ee/analytics/shared/store/modules/filters/mutations';
import * as types from 'ee/analytics/shared/store/modules/filters/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { filterMilestones, filterUsers, filterLabels } from '../../../mock_data';
import { filterMilestones, filterUsers, filterLabels } from './mock_data';
let state = null;
......@@ -10,6 +10,7 @@ const users = filterUsers.map(convertObjectPropsToCamelCase);
const labels = filterLabels.map(convertObjectPropsToCamelCase);
describe('Filters mutations', () => {
const errorCode = 500;
beforeEach(() => {
state = {
authors: { selected: null },
......@@ -50,23 +51,31 @@ describe('Filters mutations', () => {
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'errorCode'} | ${null}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'data'} | ${[]}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${errorCode}
${types.REQUEST_AUTHORS} | ${'authors'} | ${'isLoading'} | ${true}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'data'} | ${users}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'errorCode'} | ${null}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'data'} | ${[]}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${errorCode}
${types.REQUEST_LABELS} | ${'labels'} | ${'isLoading'} | ${true}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'data'} | ${labels}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'errorCode'} | ${null}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'data'} | ${[]}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${errorCode}
${types.REQUEST_ASSIGNEES} | ${'assignees'} | ${'isLoading'} | ${true}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'data'} | ${users}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'errorCode'} | ${null}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'data'} | ${[]}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${errorCode}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state, value);
......
import { mount } from '@vue/test-utils';
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import {
......@@ -9,13 +13,32 @@ import {
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
DEFAULT_LABELS,
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) =>
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
const createComponent = ({
config = mockLabelToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
} = {}) =>
mount(LabelToken, {
propsData: {
config,
......@@ -26,15 +49,7 @@ const createComponent = ({ config = mockLabelToken, value = { data: '' }, active
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
stubs: {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
},
stubs,
});
describe('LabelToken', () => {
......@@ -43,7 +58,6 @@ describe('LabelToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent();
});
afterEach(() => {
......@@ -96,6 +110,10 @@ describe('LabelToken', () => {
});
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('fetchLabelBySearchTerm', () => {
it('calls `config.fetchLabels` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels');
......@@ -138,6 +156,8 @@ describe('LabelToken', () => {
});
describe('template', () => {
const defaultLabels = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
......@@ -164,5 +184,43 @@ describe('LabelToken', () => {
.attributes('style'),
).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
});
it('renders provided defaultLabels as suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken, defaultLabels },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultLabels.length);
defaultLabels.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
it('renders `DEFAULT_LABELS` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
DEFAULT_LABELS.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
});
});
import { mount } from '@vue/test-utils';
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import {
......@@ -16,10 +21,21 @@ import {
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
const createComponent = ({
config = mockMilestoneToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
} = {}) =>
mount(MilestoneToken, {
propsData: {
......@@ -31,15 +47,7 @@ const createComponent = ({
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
stubs: {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
},
stubs,
});
describe('MilestoneToken', () => {
......@@ -126,6 +134,8 @@ describe('MilestoneToken', () => {
});
describe('template', () => {
const defaultMilestones = [{ text: 'foo', value: 'foo' }, { text: 'bar', value: 'baz' }];
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
......@@ -146,5 +156,43 @@ describe('MilestoneToken', () => {
expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
});
it('renders provided defaultMilestones as suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockMilestoneToken, defaultMilestones },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultMilestones.length);
defaultMilestones.forEach((milestone, index) => {
expect(suggestions.at(index).text()).toBe(milestone.text);
});
});
it('renders `DEFAULT_MILESTONES` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockMilestoneToken },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length);
DEFAULT_MILESTONES.forEach((milestone, index) => {
expect(suggestions.at(index).text()).toBe(milestone.text);
});
});
});
});
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