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