Commit d927b0c8 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch...

Merge branch '232465-mlunoe-share-filter-store-module-between-code-review-and-value-stream-analytics' into 'master'

Refactor(Analytics): introduce store filter module

Closes #232465

See merge request gitlab-org/gitlab!40004
parents c810f190 90c74431
/* 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