Commit 89bff282 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '217943-vsa-update-vuex-state-with-tokens' into 'master'

Update vuex state when filter bar values change

See merge request gitlab-org/gitlab!34598
parents 9bd1e55b 941c2341
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PROJECTS_PER_PAGE, STAGE_ACTIONS } from '../constants'; import { PROJECTS_PER_PAGE, STAGE_ACTIONS } from '../constants';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue'; import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
...@@ -40,7 +39,7 @@ export default { ...@@ -40,7 +39,7 @@ export default {
MetricCard, MetricCard,
FilterBar, FilterBar,
}, },
mixins: [glFeatureFlagsMixin(), UrlSyncMixin], mixins: [UrlSyncMixin],
props: { props: {
emptyStateSvgPath: { emptyStateSvgPath: {
type: String, type: String,
...@@ -58,14 +57,6 @@ export default { ...@@ -58,14 +57,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
milestonesPath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -76,6 +67,10 @@ export default { ...@@ -76,6 +67,10 @@ export default {
'selectedGroup', 'selectedGroup',
'selectedProjects', 'selectedProjects',
'selectedStage', 'selectedStage',
'selectedMilestone',
'selectedAuthor',
'selectedLabels',
'selectedAssignees',
'stages', 'stages',
'summary', 'summary',
'currentStageEvents', 'currentStageEvents',
...@@ -110,7 +105,12 @@ export default { ...@@ -110,7 +105,12 @@ export default {
return !this.hasNoAccessError && !this.isLoading; return !this.hasNoAccessError && !this.isLoading;
}, },
shouldDsiplayPathNavigation() { shouldDsiplayPathNavigation() {
return this.featureFlags.hasPathNavigation && !this.hasNoAccessError; return this.featureFlags.hasPathNavigation && !this.hasNoAccessError && this.selectedStage;
},
shouldDisplayFilterBar() {
// TODO: After we remove instance VSA currentGroupPath will be always set
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
return this.featureFlags.hasFilterBar && this.currentGroupPath;
}, },
isLoadingTypeOfWork() { isLoadingTypeOfWork() {
return this.isLoadingTasksByTypeChartTopLabels || this.isLoadingTasksByTypeChart; return this.isLoadingTasksByTypeChartTopLabels || this.isLoadingTasksByTypeChart;
...@@ -124,6 +124,10 @@ export default { ...@@ -124,6 +124,10 @@ export default {
'project_ids[]': this.selectedProjectIds, 'project_ids[]': this.selectedProjectIds,
created_after: toYmd(this.startDate), created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate), created_before: toYmd(this.endDate),
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
'label_name[]': this.selectedLabels,
'assignee_username[]': this.selectedAssignees,
}; };
}, },
stageCount() { stageCount() {
...@@ -133,27 +137,7 @@ export default { ...@@ -133,27 +137,7 @@ export default {
return this.selectedProjectIds.length > 0; return this.selectedProjectIds.length > 0;
}, },
}, },
mounted() {
const {
labelsPath,
milestonesPath,
glFeatures: {
cycleAnalyticsScatterplotEnabled: hasDurationChart,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian,
valueStreamAnalyticsPathNavigation: hasPathNavigation,
valueStreamAnalyticsFilterBar: hasFilterBar,
},
} = this;
this.setFeatureFlags({
hasDurationChart,
hasDurationChartMedian,
hasPathNavigation,
hasFilterBar,
});
this.setPaths({ labelsPath, milestonesPath });
},
methods: { methods: {
...mapActions([ ...mapActions([
'fetchCycleAnalyticsData', 'fetchCycleAnalyticsData',
...@@ -164,9 +148,9 @@ export default { ...@@ -164,9 +148,9 @@ export default {
'setDateRange', 'setDateRange',
'updateStage', 'updateStage',
'removeStage', 'removeStage',
'setFeatureFlags',
'updateStage', 'updateStage',
'reorderStage', 'reorderStage',
'setSelectedFilters',
]), ]),
...mapActions('customStages', [ ...mapActions('customStages', [
'hideForm', 'hideForm',
...@@ -175,7 +159,6 @@ export default { ...@@ -175,7 +159,6 @@ export default {
'createStage', 'createStage',
'clearFormErrors', 'clearFormErrors',
]), ]),
...mapActions('filters', ['setPaths']),
onGroupSelect(group) { onGroupSelect(group) {
this.setSelectedGroup(group); this.setSelectedGroup(group);
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
...@@ -264,7 +247,7 @@ export default { ...@@ -264,7 +247,7 @@ export default {
/> />
</div> </div>
<filter-bar <filter-bar
v-if="featureFlags.hasFilterBar" v-if="shouldDisplayFilterBar"
class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 mt-md-0 gl-mr-3" class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 mt-md-0 gl-mr-3"
:disabled="!hasProject" :disabled="!hasProject"
/> />
......
...@@ -6,6 +6,24 @@ import MilestoneToken from '../../shared/components/tokens/milestone_token.vue'; ...@@ -6,6 +6,24 @@ 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 UserToken from '../../shared/components/tokens/user_token.vue'; import UserToken from '../../shared/components/tokens/user_token.vue';
export const prepareTokens = ({
milestone = null,
author = null,
assignees = [],
labels = [],
} = {}) => {
const authorToken = author ? [{ type: 'author', value: { data: author } }] : [];
const milestoneToken = milestone ? [{ type: 'milestone', value: { data: milestone } }] : [];
const assigneeTokens = assignees?.length
? assignees.map(data => ({ type: 'assignees', value: { data } }))
: [];
const labelTokens = labels?.length
? labels.map(data => ({ type: 'labels', value: { data } }))
: [];
return [...authorToken, ...milestoneToken, ...assigneeTokens, ...labelTokens];
};
export default { export default {
name: 'FilterBar', name: 'FilterBar',
components: { components: {
...@@ -33,6 +51,7 @@ export default { ...@@ -33,6 +51,7 @@ export default {
authorsLoading: state => state.authors.isLoading, authorsLoading: state => state.authors.isLoading,
assignees: state => state.assignees.data, assignees: state => state.assignees.data,
assigneesLoading: state => state.assignees.isLoading, assigneesLoading: state => state.assignees.isLoading,
initialTokens: state => state.initialTokens,
}), }),
availableTokens() { availableTokens() {
return [ return [
...@@ -81,8 +100,21 @@ export default { ...@@ -81,8 +100,21 @@ export default {
]; ];
}, },
}, },
mounted() {
this.initializeTokens();
},
methods: { methods: {
...mapActions('filters', ['setFilters']), ...mapActions('filters', ['setFilters', 'fetchTokenData']),
initializeTokens() {
const {
selectedMilestone: milestone = null,
selectedAuthor: author = null,
selectedAssignees: assignees = [],
selectedLabels: labels = [],
} = this.initialTokens;
const preparedTokens = prepareTokens({ milestone, author, assignees, labels });
this.value = preparedTokens;
},
processFilters(filters) { processFilters(filters) {
return filters.reduce((acc, token) => { return filters.reduce((acc, token) => {
const { type, value } = token; const { type, value } = token;
......
...@@ -6,18 +6,20 @@ import { parseBoolean } from '~/lib/utils/common_utils'; ...@@ -6,18 +6,20 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default () => { export default () => {
const el = document.querySelector('#js-cycle-analytics-app'); const el = document.querySelector('#js-cycle-analytics-app');
const { const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath, hideGroupDropDown } = el.dataset;
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
hideGroupDropDown,
milestonesPath = '',
labelsPath = '',
} = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset); const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore(); const store = createStore();
store.dispatch('initializeCycleAnalytics', initialData); const {
cycleAnalyticsScatterplotEnabled: hasDurationChart = false,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian = false,
valueStreamAnalyticsPathNavigation: hasPathNavigation = false,
valueStreamAnalyticsFilterBar: hasFilterBar = false,
} = gon?.features;
store.dispatch('initializeCycleAnalytics', {
...initialData,
featureFlags: { hasDurationChart, hasDurationChartMedian, hasPathNavigation, hasFilterBar },
});
return new Vue({ return new Vue({
el, el,
...@@ -30,8 +32,6 @@ export default () => { ...@@ -30,8 +32,6 @@ export default () => {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
hideGroupDropDown: parseBoolean(hideGroupDropDown), hideGroupDropDown: parseBoolean(hideGroupDropDown),
milestonesPath,
labelsPath,
}, },
}), }),
}); });
......
...@@ -8,11 +8,19 @@ import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../ut ...@@ -8,11 +8,19 @@ import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../ut
export const setFeatureFlags = ({ commit }, featureFlags) => export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags); commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group); export const setSelectedGroup = ({ commit, dispatch, state }, group) => {
commit(types.SET_SELECTED_GROUP, group);
const { featureFlags } = state;
if (featureFlags?.hasFilterBar) {
return dispatch('filters/initialize', {
groupPath: group.full_path,
});
}
return Promise.resolve();
};
export const setSelectedProjects = ({ commit }, projects) => { export const setSelectedProjects = ({ commit }, projects) =>
commit(types.SET_SELECTED_PROJECTS, projects); commit(types.SET_SELECTED_PROJECTS, projects);
};
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage); export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
...@@ -234,20 +242,36 @@ export const removeStage = ({ dispatch, state }, stageId) => { ...@@ -234,20 +242,36 @@ export const removeStage = ({ dispatch, state }, stageId) => {
.catch(error => dispatch('receiveRemoveStageError', error)); .catch(error => dispatch('receiveRemoveStageError', error));
}; };
export const setSelectedFilters = ({ commit }, filters) => export const setSelectedFilters = ({ commit, dispatch, getters }, filters = {}) => {
commit(types.SET_SELECTED_FILTERS, filters); commit(types.SET_SELECTED_FILTERS, filters);
const { currentGroupPath } = getters;
if (currentGroupPath) {
return dispatch('fetchCycleAnalyticsData');
}
return Promise.resolve();
};
export const initializeCycleAnalyticsSuccess = ({ commit }) => export const initializeCycleAnalyticsSuccess = ({ commit }) =>
commit(types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS); commit(types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS);
export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) => { export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) => {
commit(types.INITIALIZE_CYCLE_ANALYTICS, initialData); commit(types.INITIALIZE_CYCLE_ANALYTICS, initialData);
if (initialData?.group?.fullPath) {
const { featureFlags = {} } = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags);
if (initialData.group?.fullPath) {
if (featureFlags?.hasFilterBar) {
dispatch('filters/initialize', {
groupPath: initialData.group.fullPath,
...initialData,
});
}
return dispatch('fetchCycleAnalyticsData').then(() => return dispatch('fetchCycleAnalyticsData').then(() =>
dispatch('initializeCycleAnalyticsSuccess'), dispatch('initializeCycleAnalyticsSuccess'),
); );
} }
return dispatch('initializeCycleAnalyticsSuccess'); return dispatch('initializeCycleAnalyticsSuccess');
}; };
......
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`); const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ commit }, { milestonesPath = '', labelsPath = '' }) => { // TODO: After we remove instance VSA we can rely on the paths from the BE
commit(types.SET_MILESTONES_PATH, appendExtension(milestonesPath)); // https://gitlab.com/gitlab-org/gitlab/-/issues/223735
commit(types.SET_LABELS_PATH, appendExtension(labelsPath)); export const setPaths = ({ commit }, { groupPath = '', milestonesPath = '', labelsPath = '' }) => {
const ms = milestonesPath || `/groups/${groupPath}/-/milestones`;
const ls = labelsPath || `/groups/${groupPath}/-/labels`;
commit(types.SET_MILESTONES_PATH, appendExtension(ms));
commit(types.SET_LABELS_PATH, appendExtension(ls));
}; };
export const setFilters = ({ dispatch, state }, params) => { export const fetchTokenData = ({ dispatch }) => {
const { selectedLabels: labelNames = [], ...rest } = params; return Promise.all([
const { dispatch('fetchLabels'),
labels: { data: labelsList = [] }, dispatch('fetchMilestones'),
} = state; dispatch('fetchAuthors'),
dispatch('fetchAssignees'),
]);
};
export const fetchMilestones = ({ commit, state }) => {
commit(types.REQUEST_MILESTONES);
const { milestonesPath } = state;
return axios
.get(milestonesPath)
.then(({ data }) => commit(types.RECEIVE_MILESTONES_SUCCESS, data))
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
createFlash(__('Failed to load milestones. Please try again.'));
});
};
export const fetchLabels = ({ commit, state }) => {
commit(types.REQUEST_LABELS);
return axios
.get(state.labelsPath)
.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.'));
});
};
const fetchUser = ({ commit, endpoint, query, action, errorMessage }) => {
commit(`REQUEST_${action}`);
return Api.groupMembers(endpoint, { query })
.then(({ data }) => commit(`RECEIVE_${action}_SUCCESS`, data))
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
createFlash(errorMessage);
});
};
export const fetchAuthors = ({ commit, rootGetters }, query = '') => {
const { currentGroupPath } = rootGetters;
return fetchUser({
commit,
query,
endpoint: currentGroupPath,
action: 'AUTHORS',
errorMessage: __('Failed to load authors. Please try again.'),
});
};
export const fetchAssignees = ({ commit, rootGetters }, query = '') => {
const { currentGroupPath } = rootGetters;
return fetchUser({
commit,
query,
endpoint: currentGroupPath,
action: 'ASSIGNEES',
errorMessage: __('Failed to load assignees. Please try again.'),
});
};
const selectedLabels = labelsList.filter(({ title }) => labelNames.includes(title)); export const setFilters = ({ dispatch }, nextFilters) =>
const nextFilters = { dispatch('setSelectedFilters', nextFilters, { root: true });
...rest,
selectedLabels,
};
return dispatch('setSelectedFilters', nextFilters, { root: true }); export const initialize = ({ dispatch, commit }, initialFilters) => {
commit(types.INITIALIZE, initialFilters);
return dispatch('setPaths', initialFilters)
.then(() => dispatch('setFilters', initialFilters))
.then(() => dispatch('fetchTokenData'));
}; };
export const INITIALIZE = 'INITIALIZE';
export const SET_MILESTONES_PATH = 'SET_MILESTONES_PATH'; export const SET_MILESTONES_PATH = 'SET_MILESTONES_PATH';
export const SET_LABELS_PATH = 'SET_LABELS_PATH'; export const SET_LABELS_PATH = 'SET_LABELS_PATH';
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 REQUEST_AUTHORS = 'REQUEST_AUTHORS';
export const RECEIVE_AUTHORS_SUCCESS = 'RECEIVE_AUTHORS_SUCCESS';
export const RECEIVE_AUTHORS_ERROR = 'RECEIVE_AUTHORS_ERROR';
export const REQUEST_ASSIGNEES = 'REQUEST_ASSIGNEES';
export const RECEIVE_ASSIGNEES_SUCCESS = 'RECEIVE_ASSIGNEES_SUCCESS';
export const RECEIVE_ASSIGNEES_ERROR = 'RECEIVE_ASSIGNEES_ERROR';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.INITIALIZE](
state,
{
selectedAuthor = null,
selectedMilestone = null,
selectedAssignees = [],
selectedLabels = [],
} = {},
) {
state.initialTokens = {
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
};
},
[types.SET_MILESTONES_PATH](state, milestonesPath) { [types.SET_MILESTONES_PATH](state, milestonesPath) {
state.milestonesPath = milestonesPath; state.milestonesPath = milestonesPath;
}, },
[types.SET_LABELS_PATH](state, labelsPath) { [types.SET_LABELS_PATH](state, labelsPath) {
state.labelsPath = labelsPath; state.labelsPath = labelsPath;
}, },
[types.REQUEST_MILESTONES](state) {
state.milestones.isLoading = true;
},
[types.RECEIVE_MILESTONES_SUCCESS](state, data) {
state.milestones.isLoading = false;
state.milestones.data = data;
},
[types.RECEIVE_MILESTONES_ERROR](state) {
state.milestones.isLoading = false;
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;
},
[types.RECEIVE_LABELS_ERROR](state) {
state.labels.isLoading = false;
state.labels.data = [];
},
[types.REQUEST_AUTHORS](state) {
state.authors.isLoading = true;
},
[types.RECEIVE_AUTHORS_SUCCESS](state, data) {
state.authors.isLoading = false;
state.authors.data = data;
},
[types.RECEIVE_AUTHORS_ERROR](state) {
state.authors.isLoading = false;
state.authors.data = [];
},
[types.REQUEST_ASSIGNEES](state) {
state.assignees.isLoading = true;
},
[types.RECEIVE_ASSIGNEES_SUCCESS](state, data) {
state.assignees.isLoading = false;
state.assignees.data = data;
},
[types.RECEIVE_ASSIGNEES_ERROR](state) {
state.assignees.isLoading = false;
state.assignees.data = [];
},
}; };
...@@ -17,4 +17,10 @@ export default () => ({ ...@@ -17,4 +17,10 @@ export default () => ({
isLoading: false, isLoading: false,
data: [], data: [],
}, },
initialTokens: {
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
},
}); });
...@@ -115,5 +115,11 @@ export default { ...@@ -115,5 +115,11 @@ export default {
state.isSavingStageOrder = false; state.isSavingStageOrder = false;
state.errorSavingStageOrder = true; state.errorSavingStageOrder = true;
}, },
[types.SET_SELECTED_FILTERS]() {}, [types.SET_SELECTED_FILTERS](state, params) {
const { selectedAuthor, selectedAssignees, selectedMilestone, selectedLabels } = params;
state.selectedAuthor = selectedAuthor;
state.selectedAssignees = selectedAssignees;
state.selectedMilestone = selectedMilestone;
state.selectedLabels = selectedLabels;
},
}; };
...@@ -16,6 +16,10 @@ export default () => ({ ...@@ -16,6 +16,10 @@ export default () => ({
selectedGroup: null, selectedGroup: null,
selectedProjects: [], selectedProjects: [],
selectedStage: null, selectedStage: null,
selectedAuthor: null,
selectedMilestone: null,
selectedAssignees: [],
selectedLabels: [],
currentStageEvents: [], currentStageEvents: [],
......
...@@ -30,12 +30,15 @@ export default { ...@@ -30,12 +30,15 @@ export default {
return this.config.labels; return this.config.labels;
}, },
filteredLabels() { filteredLabels() {
return this.labels const labelsList = this.labels.map(label => ({
.filter(label => label.title.toLowerCase().indexOf(this.value.data?.toLowerCase()) !== -1) ...label,
.map(label => ({ value: this.getEscapedText(label.title),
...label, }));
value: this.getEscapedText(label.title), return this.value?.data
})); ? labelsList.filter(
label => label.title.toLowerCase().indexOf(this.value.data?.toLowerCase()) !== -1,
)
: labelsList;
}, },
}, },
methods: { methods: {
......
...@@ -79,6 +79,12 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -79,6 +79,12 @@ export const buildCycleAnalyticsInitialData = ({
groupFullPath = null, groupFullPath = null,
groupParentId = null, groupParentId = null,
groupAvatarUrl = null, groupAvatarUrl = null,
author = null,
milestone = null,
labels = null,
assignees = null,
labelsPath = '',
milestonesPath = '',
} = {}) => ({ } = {}) => ({
group: groupId group: groupId
? convertObjectPropsToCamelCase( ? convertObjectPropsToCamelCase(
...@@ -96,6 +102,12 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -96,6 +102,12 @@ export const buildCycleAnalyticsInitialData = ({
selectedProjects: projects selectedProjects: projects
? buildProjectsFromJSON(projects).map(convertObjectPropsToCamelCase) ? buildProjectsFromJSON(projects).map(convertObjectPropsToCamelCase)
: [], : [],
selectedAuthor: author,
selectedMilestone: milestone,
selectedLabels: labels ? JSON.parse(labels) : [],
selectedAssignees: assignees ? JSON.parse(assignees) : [],
labelsPath,
milestonesPath,
}); });
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => { export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
......
...@@ -30,8 +30,6 @@ import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin'; ...@@ -30,8 +30,6 @@ import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access'; const noAccessSvgPath = 'path/to/no/access';
const emptyStateSvgPath = 'path/to/empty/state'; const emptyStateSvgPath = 'path/to/empty/state';
const milestonesPath = '/some/milestones/endpoint';
const labelsPath = '/some/labels/endpoint';
const hideGroupDropDown = false; const hideGroupDropDown = false;
const selectedGroup = convertObjectPropsToCamelCase(mockData.group); const selectedGroup = convertObjectPropsToCamelCase(mockData.group);
...@@ -48,15 +46,30 @@ const defaultStubs = { ...@@ -48,15 +46,30 @@ const defaultStubs = {
GroupsDropdownFilter: true, GroupsDropdownFilter: true,
}; };
const defaultFeatureFlags = {
hasDurationChart: true,
hasDurationChartMedian: true,
hasPathNavigation: false,
hasFilterBar: false,
};
const initialCycleAnalyticsState = {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
group: selectedGroup,
};
function createComponent({ function createComponent({
opts = { opts = {
stubs: defaultStubs, stubs: defaultStubs,
}, },
shallow = true, shallow = true,
withStageSelected = false, withStageSelected = false,
scatterplotEnabled = true, featureFlags = {},
pathNavigationEnabled = false,
filterBarEnabled = false,
props = {}, props = {},
} = {}) { } = {}) {
const func = shallow ? shallowMount : mount; const func = shallow ? shallowMount : mount;
...@@ -68,29 +81,24 @@ function createComponent({ ...@@ -68,29 +81,24 @@ function createComponent({
emptyStateSvgPath, emptyStateSvgPath,
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
milestonesPath,
labelsPath,
baseStagesEndpoint: mockData.endpoints.baseStagesEndpoint, baseStagesEndpoint: mockData.endpoints.baseStagesEndpoint,
hideGroupDropDown, hideGroupDropDown,
...props, ...props,
}, },
provide: {
glFeatures: {
cycleAnalyticsScatterplotEnabled: scatterplotEnabled,
valueStreamAnalyticsPathNavigation: pathNavigationEnabled,
valueStreamAnalyticsFilterBar: filterBarEnabled,
},
},
...opts, ...opts,
}); });
comp.vm.$store.dispatch('initializeCycleAnalytics', { comp.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate, createdAfter: mockData.startDate,
createdBefore: mockData.endDate, createdBefore: mockData.endDate,
featureFlags: {
...defaultFeatureFlags,
...featureFlags,
},
}); });
if (withStageSelected) { if (withStageSelected) {
comp.vm.$store.dispatch('setSelectedGroup', { comp.vm.$store.commit('SET_SELECTED_GROUP', {
...selectedGroup, ...selectedGroup,
}); });
...@@ -163,11 +171,11 @@ describe('Cycle Analytics component', () => { ...@@ -163,11 +171,11 @@ describe('Cycle Analytics component', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent(); wrapper = createComponent({
featureFlags: {
wrapper.vm.$store.dispatch('initializeCycleAnalytics', { hasPathNavigation: true,
createdAfter: mockData.startDate, hasFilterBar: true,
createdBefore: mockData.endDate, },
}); });
}); });
...@@ -247,6 +255,10 @@ describe('Cycle Analytics component', () => { ...@@ -247,6 +255,10 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ wrapper = createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
hasFilterBar: true,
},
}); });
}); });
...@@ -311,6 +323,15 @@ describe('Cycle Analytics component', () => { ...@@ -311,6 +323,15 @@ describe('Cycle Analytics component', () => {
describe('path navigation', () => { describe('path navigation', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: false,
},
});
});
it('does not display the path navigation', () => { it('does not display the path navigation', () => {
displaysPathNavigation(false); displaysPathNavigation(false);
}); });
...@@ -320,7 +341,9 @@ describe('Cycle Analytics component', () => { ...@@ -320,7 +341,9 @@ describe('Cycle Analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
withStageSelected: true, withStageSelected: true,
pathNavigationEnabled: true, featureFlags: {
hasPathNavigation: true,
},
}); });
}); });
...@@ -332,6 +355,15 @@ describe('Cycle Analytics component', () => { ...@@ -332,6 +355,15 @@ describe('Cycle Analytics component', () => {
describe('filter bar', () => { describe('filter bar', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
featureFlags: {
hasFilterBar: false,
},
});
});
it('does not display the filter bar', () => { it('does not display the filter bar', () => {
displaysFilterBar(false); displaysFilterBar(false);
}); });
...@@ -341,7 +373,9 @@ describe('Cycle Analytics component', () => { ...@@ -341,7 +373,9 @@ describe('Cycle Analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
withStageSelected: true, withStageSelected: true,
filterBarEnabled: true, featureFlags: {
hasFilterBar: true,
},
}); });
}); });
...@@ -354,6 +388,7 @@ describe('Cycle Analytics component', () => { ...@@ -354,6 +388,7 @@ describe('Cycle Analytics component', () => {
describe('StageTable', () => { describe('StageTable', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ wrapper = createComponent({
opts: { opts: {
stubs: { stubs: {
...@@ -601,30 +636,31 @@ describe('Cycle Analytics component', () => { ...@@ -601,30 +636,31 @@ describe('Cycle Analytics component', () => {
name: 'New test group', name: 'New test group',
}; };
const defaultParams = {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
'project_ids[]': [],
milestone_title: null,
author_username: null,
'assignee_username[]': [],
'label_name[]': [],
};
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
beforeEach(() => { beforeEach(() => {
commonUtils.historyPushState = jest.fn(); commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn(); urlUtils.setUrlParams = jest.fn();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent();
wrapper = createComponent({ wrapper.vm.$store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
shallow: false,
scatterplotEnabled: false,
stubs: {
...defaultStubs,
},
});
return wrapper.vm.$nextTick();
}); });
it('sets the created_after and created_before url parameters', () => { it('sets the created_after and created_before url parameters', () => {
return shouldSetUrlParams({ return shouldSetUrlParams(defaultParams);
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: null,
'project_ids[]': [],
});
}); });
describe('with hideGroupDropDown=true', () => { describe('with hideGroupDropDown=true', () => {
...@@ -635,31 +671,23 @@ describe('Cycle Analytics component', () => { ...@@ -635,31 +671,23 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ wrapper = createComponent({
shallow: false,
scatterplotEnabled: false,
stubs: {
...defaultStubs,
},
props: { props: {
hideGroupDropDown: true, hideGroupDropDown: true,
}, },
}); });
wrapper.vm.$store.dispatch('initializeCycleAnalytics', { wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate, ...initialCycleAnalyticsState,
createdBefore: mockData.endDate,
group: fakeGroup, group: fakeGroup,
}); });
return wrapper.vm.$nextTick();
}); });
it('sets the group_id url parameter', () => { it('sets the group_id url parameter', () => {
return shouldSetUrlParams({ return shouldSetUrlParams({
...defaultParams,
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
group_id: null, group_id: null,
'project_ids[]': [],
}); });
}); });
}); });
...@@ -669,34 +697,29 @@ describe('Cycle Analytics component', () => { ...@@ -669,34 +697,29 @@ describe('Cycle Analytics component', () => {
wrapper.vm.$store.dispatch('setSelectedGroup', { wrapper.vm.$store.dispatch('setSelectedGroup', {
...fakeGroup, ...fakeGroup,
}); });
return wrapper.vm.$nextTick();
}); });
it('sets the group_id url parameter', () => { it('sets the group_id url parameter', () => {
return shouldSetUrlParams({ return shouldSetUrlParams({
created_after: toYmd(mockData.startDate), ...defaultParams,
created_before: toYmd(mockData.endDate),
group_id: fakeGroup.fullPath, group_id: fakeGroup.fullPath,
'project_ids[]': [],
}); });
}); });
}); });
describe('with a group and selectedProjectIds set', () => { describe('with a group and selectedProjectIds set', () => {
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
beforeEach(() => { beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', { wrapper.vm.$store.dispatch('setSelectedGroup', {
...selectedGroup, ...selectedGroup,
}); });
wrapper.vm.$store.dispatch('setSelectedProjects', mockData.selectedProjects); wrapper.vm.$store.dispatch('setSelectedProjects', mockData.selectedProjects);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('sets the project_ids url parameter', () => { it('sets the project_ids url parameter', () => {
return shouldSetUrlParams({ return shouldSetUrlParams({
...defaultParams,
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath, group_id: selectedGroup.fullPath,
...@@ -704,5 +727,28 @@ describe('Cycle Analytics component', () => { ...@@ -704,5 +727,28 @@ describe('Cycle Analytics component', () => {
}); });
}); });
}); });
describe.each`
stateKey | payload | paramKey
${'selectedMilestone'} | ${'12.0'} | ${'milestone_title'}
${'selectedAuthor'} | ${'rootUser'} | ${'author_username'}
${'selectedAssignees'} | ${['rootUser', 'secondaryUser']} | ${'assignee_username[]'}
${'selectedLabels'} | ${['Afternix', 'Brouceforge']} | ${'label_name[]'}
`('with a $stateKey updates the $paramKey url parameter', ({ stateKey, payload, paramKey }) => {
beforeEach(() => {
wrapper.vm.$store.dispatch('filters/setFilters', {
...initialCycleAnalyticsState,
group: selectedGroup,
selectedProjects: mockData.selectedProjects,
[stateKey]: payload,
});
});
it(`sets the ${paramKey} url parameter`, () => {
return shouldSetUrlParams({
...defaultParams,
[paramKey]: payload,
});
});
});
}); });
}); });
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue'; import FilterBar, { prepareTokens } 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/cycle_analytics/store/modules/filters/state';
import { filterMilestones, filterLabels } from '../mock_data'; import { filterMilestones, filterLabels } from '../mock_data';
...@@ -57,11 +57,15 @@ describe('Filter bar', () => { ...@@ -57,11 +57,15 @@ describe('Filter bar', () => {
.props('availableTokens') .props('availableTokens')
.filter(token => token.type === type)[0]; .filter(token => token.type === type)[0];
it('renders GlFilteredSearch component', () => { describe('default', () => {
store = createStore(); beforeEach(() => {
wrapper = createComponent(store); store = createStore();
wrapper = createComponent(store);
});
expect(findFilteredSearch().exists()).toBe(true); it('renders GlFilteredSearch component', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
}); });
describe('when the state has data', () => { describe('when the state has data', () => {
...@@ -176,4 +180,27 @@ describe('Filter bar', () => { ...@@ -176,4 +180,27 @@ describe('Filter bar', () => {
); );
}); });
}); });
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);
});
});
}); });
...@@ -56,11 +56,10 @@ describe('Cycle analytics actions', () => { ...@@ -56,11 +56,10 @@ describe('Cycle analytics actions', () => {
it.each` it.each`
action | type | stateKey | payload action | type | stateKey | payload
${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }} ${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }}
${'setSelectedGroup'} | ${'SET_SELECTED_GROUP'} | ${'selectedGroup'} | ${'someNewGroup'}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]} ${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }} ${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => { `('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
testAction( return testAction(
actions[action], actions[action],
payload, payload,
state, state,
...@@ -88,6 +87,48 @@ describe('Cycle analytics actions', () => { ...@@ -88,6 +87,48 @@ describe('Cycle analytics actions', () => {
}); });
}); });
describe('setSelectedGroup', () => {
it('commits the setSelectedGroup mutation', () => {
return testAction(
actions.setSelectedGroup,
{ ...selectedGroup },
state,
[{ type: types.SET_SELECTED_GROUP, payload: selectedGroup }],
[],
);
});
describe('with hasFilterBar=true', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasFilterBar: true,
},
};
mock = new MockAdapter(axios);
});
it('commits the setSelectedGroup mutation', () => {
return testAction(
actions.setSelectedGroup,
{ full_path: selectedGroup.fullPath },
state,
[{ type: types.SET_SELECTED_GROUP, payload: { full_path: selectedGroup.fullPath } }],
[
{
type: 'filters/initialize',
payload: {
groupPath: selectedGroup.fullPath,
},
},
],
);
});
});
});
describe('fetchStageData', () => { describe('fetchStageData', () => {
beforeEach(() => { beforeEach(() => {
state = { ...state, selectedGroup }; state = { ...state, selectedGroup };
...@@ -198,7 +239,7 @@ describe('Cycle analytics actions', () => { ...@@ -198,7 +239,7 @@ describe('Cycle analytics actions', () => {
}); });
it(`dispatches actions for required value stream analytics analytics data`, () => { it(`dispatches actions for required value stream analytics analytics data`, () => {
testAction( return testAction(
actions.fetchCycleAnalyticsData, actions.fetchCycleAnalyticsData,
state, state,
null, null,
...@@ -648,9 +689,7 @@ describe('Cycle analytics actions', () => { ...@@ -648,9 +689,7 @@ describe('Cycle analytics actions', () => {
}); });
describe('receiveStageMedianValuesError', () => { describe('receiveStageMedianValuesError', () => {
beforeEach(() => {}); it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () =>
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () => {
testAction( testAction(
actions.receiveStageMedianValuesError, actions.receiveStageMedianValuesError,
null, null,
...@@ -661,8 +700,7 @@ describe('Cycle analytics actions', () => { ...@@ -661,8 +700,7 @@ describe('Cycle analytics actions', () => {
}, },
], ],
[], [],
); ));
});
it('will flash an error message', () => { it('will flash an error message', () => {
actions.receiveStageMedianValuesError({ commit: () => {} }); actions.receiveStageMedianValuesError({ commit: () => {} });
......
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 httpStatusCodes from '~/lib/utils/http_status';
import * as actions from 'ee/analytics/cycle_analytics/store/modules/filters/actions'; import * as actions from 'ee/analytics/cycle_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types';
import initialState from 'ee/analytics/cycle_analytics/store/modules/filters/state'; import initialState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
import { filterLabels } from '../../../mock_data'; import createFlash from '~/flash';
import { filterMilestones, filterUsers, filterLabels } from '../../../mock_data';
const milestonesPath = 'fake_milestones_path'; const milestonesPath = 'fake_milestones_path';
const labelsPath = 'fake_labels_path'; const labelsPath = 'fake_labels_path';
...@@ -14,16 +16,63 @@ jest.mock('~/flash', () => jest.fn()); ...@@ -14,16 +16,63 @@ jest.mock('~/flash', () => jest.fn());
describe('Filters actions', () => { describe('Filters actions', () => {
let state; let state;
let mock; let mock;
let mockDispatch;
let mockCommit;
beforeEach(() => { beforeEach(() => {
state = initialState(); state = initialState();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockDispatch = jest.fn().mockResolvedValue();
mockCommit = jest.fn();
}); });
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); });
describe('initialize', () => {
const initialData = {
milestonesPath,
labelsPath,
selectedAuthor: 'Mr cool',
selectedMilestone: 'NEXT',
};
it('dispatches setPaths, setFilters and fetchTokenData', () => {
return actions
.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
)
.then(() => {
expect(mockDispatch).toHaveBeenCalledTimes(3);
expect(mockDispatch).toHaveBeenCalledWith('setPaths', initialData);
expect(mockDispatch).toHaveBeenCalledWith('setFilters', initialData);
expect(mockDispatch).toHaveBeenCalledWith('fetchTokenData');
});
});
it(`commits the ${types.INITIALIZE}`, () => {
return actions
.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
)
.then(() => {
expect(mockCommit).toHaveBeenCalledWith(types.INITIALIZE, initialData);
});
});
});
describe('setFilters', () => { describe('setFilters', () => {
const nextFilters = { const nextFilters = {
selectedAuthor: 'Mr cool', selectedAuthor: 'Mr cool',
...@@ -39,10 +88,7 @@ describe('Filters actions', () => { ...@@ -39,10 +88,7 @@ describe('Filters actions', () => {
[ [
{ {
type: 'setSelectedFilters', type: 'setSelectedFilters',
payload: { payload: nextFilters,
...nextFilters,
selectedLabels: [],
},
}, },
], ],
); );
...@@ -59,7 +105,7 @@ describe('Filters actions', () => { ...@@ -59,7 +105,7 @@ describe('Filters actions', () => {
type: 'setSelectedFilters', type: 'setSelectedFilters',
payload: { payload: {
...nextFilters, ...nextFilters,
selectedLabels: [filterLabels[1]], selectedLabels: [filterLabels[1].title],
}, },
}, },
], ],
...@@ -68,7 +114,7 @@ describe('Filters actions', () => { ...@@ -68,7 +114,7 @@ describe('Filters actions', () => {
}); });
describe('setPaths', () => { describe('setPaths', () => {
it('sets the api paths and dispatches requests for initial data', () => { it('sets the api paths', () => {
return testAction( return testAction(
actions.setPaths, actions.setPaths,
{ milestonesPath, labelsPath }, { milestonesPath, labelsPath },
...@@ -81,4 +127,193 @@ describe('Filters actions', () => { ...@@ -81,4 +127,193 @@ describe('Filters actions', () => {
); );
}); });
}); });
describe('fetchTokenData', () => {
it('dispatches requests for token data', () => {
return testAction(
actions.fetchTokenData,
{ milestonesPath, labelsPath },
state,
[],
[
{ type: 'fetchLabels' },
{ type: 'fetchMilestones' },
{ type: 'fetchAuthors' },
{ type: 'fetchAssignees' },
],
);
});
});
describe('fetchAuthors', () => {
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
});
it('dispatches RECEIVE_AUTHORS_SUCCESS with received data', () => {
testAction(
actions.fetchAuthors,
null,
state,
[
{ type: types.REQUEST_AUTHORS },
{ type: types.RECEIVE_AUTHORS_SUCCESS, payload: filterUsers },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_AUTHORS_ERROR', () => {
return testAction(
actions.fetchAuthors,
null,
state,
[
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
).then(() => expect(createFlash).toHaveBeenCalled());
});
});
});
describe('fetchMilestones', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(milestonesPath).replyOnce(httpStatusCodes.OK, filterMilestones);
});
it('dispatches RECEIVE_MILESTONES_SUCCESS with received data', () => {
testAction(
actions.fetchMilestones,
null,
{ ...state, milestonesPath },
[
{ type: types.REQUEST_MILESTONES },
{ type: types.RECEIVE_MILESTONES_SUCCESS, payload: filterMilestones },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_MILESTONES_ERROR', () => {
return testAction(
actions.fetchMilestones,
null,
state,
[
{ type: types.REQUEST_MILESTONES },
{
type: types.RECEIVE_MILESTONES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
).then(() => expect(createFlash).toHaveBeenCalled());
});
});
});
describe('fetchAssignees', () => {
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
});
it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data', () => {
testAction(
actions.fetchAssignees,
null,
{ ...state, milestonesPath },
[
{ type: types.REQUEST_ASSIGNEES },
{ type: types.RECEIVE_ASSIGNEES_SUCCESS, payload: filterUsers },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_ASSIGNEES_ERROR', () => {
return testAction(
actions.fetchAssignees,
null,
state,
[
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
).then(() => expect(createFlash).toHaveBeenCalled());
});
});
});
describe('fetchLabels', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(labelsPath).replyOnce(httpStatusCodes.OK, filterLabels);
});
it('dispatches RECEIVE_LABELS_SUCCESS with received data', () => {
testAction(
actions.fetchLabels,
null,
{ ...state, labelsPath },
[
{ type: types.REQUEST_LABELS },
{ type: types.RECEIVE_LABELS_SUCCESS, payload: filterLabels },
],
[],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_LABELS_ERROR', () => {
return testAction(
actions.fetchLabels,
null,
state,
[
{ type: types.REQUEST_LABELS },
{
type: types.RECEIVE_LABELS_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
).then(() => expect(createFlash).toHaveBeenCalled());
});
});
});
}); });
import mutations from 'ee/analytics/cycle_analytics/store/modules/filters/mutations'; import mutations from 'ee/analytics/cycle_analytics/store/modules/filters/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { filterMilestones, filterUsers, filterLabels } from '../../../mock_data';
let state = null; let state = null;
const milestones = filterMilestones.map(convertObjectPropsToCamelCase);
const users = filterUsers.map(convertObjectPropsToCamelCase);
const labels = filterLabels.map(convertObjectPropsToCamelCase);
describe('Filters mutations', () => { describe('Filters mutations', () => {
beforeEach(() => { beforeEach(() => {
state = { initialTokens: {}, milestones: {}, authors: {}, labels: {}, assignees: {} }; state = { initialTokens: {}, milestones: {}, authors: {}, labels: {}, assignees: {} };
...@@ -21,4 +27,44 @@ describe('Filters mutations', () => { ...@@ -21,4 +27,44 @@ describe('Filters mutations', () => {
expect(state[stateKey]).toEqual(value); expect(state[stateKey]).toEqual(value);
}); });
it.each`
mutation | rootKey | stateKey | value
${types.INITIALIZE} | ${'initialTokens'} | ${'selectedAuthor'} | ${null}
${types.INITIALIZE} | ${'initialTokens'} | ${'selectedMilestone'} | ${null}
${types.INITIALIZE} | ${'initialTokens'} | ${'selectedAssignees'} | ${[]}
${types.INITIALIZE} | ${'initialTokens'} | ${'selectedLabels'} | ${[]}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state);
expect(state[rootKey][stateKey]).toEqual(value);
});
it.each`
mutation | rootKey | stateKey | value
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'data'} | ${[]}
${types.REQUEST_AUTHORS} | ${'authors'} | ${'isLoading'} | ${true}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'data'} | ${users}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'data'} | ${[]}
${types.REQUEST_LABELS} | ${'labels'} | ${'isLoading'} | ${true}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'data'} | ${labels}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'data'} | ${[]}
${types.REQUEST_ASSIGNEES} | ${'assignees'} | ${'isLoading'} | ${true}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'data'} | ${users}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'data'} | ${[]}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state, value);
expect(state[rootKey][stateKey]).toEqual(value);
});
}); });
...@@ -76,11 +76,17 @@ describe('buildProjectFromDataset', () => { ...@@ -76,11 +76,17 @@ describe('buildProjectFromDataset', () => {
describe('buildCycleAnalyticsInitialData', () => { describe('buildCycleAnalyticsInitialData', () => {
it.each` it.each`
field | value field | value
${'group'} | ${null} ${'group'} | ${null}
${'createdBefore'} | ${null} ${'createdBefore'} | ${null}
${'createdAfter'} | ${null} ${'createdAfter'} | ${null}
${'selectedProjects'} | ${[]} ${'selectedProjects'} | ${[]}
${'selectedAuthor'} | ${null}
${'selectedMilestone'} | ${null}
${'selectedLabels'} | ${[]}
${'selectedAssignees'} | ${[]}
${'labelsPath'} | ${''}
${'milestonesPath'} | ${''}
`('will set a default value for "$field" if is not present', ({ field, value }) => { `('will set a default value for "$field" if is not present', ({ field, value }) => {
expect(buildCycleAnalyticsInitialData()).toMatchObject({ expect(buildCycleAnalyticsInitialData()).toMatchObject({
[field]: value, [field]: value,
...@@ -132,6 +138,62 @@ describe('buildCycleAnalyticsInitialData', () => { ...@@ -132,6 +138,62 @@ describe('buildCycleAnalyticsInitialData', () => {
}); });
}); });
describe('selectedAssignees', () => {
it('will be set given an array of assignees', () => {
const selectedAssignees = ['krillin', 'chiao-tzu'];
expect(
buildCycleAnalyticsInitialData({ assignees: JSON.stringify(selectedAssignees) }),
).toMatchObject({
selectedAssignees,
});
});
it.each`
field | value
${'selectedAssignees'} | ${null}
${'selectedAssignees'} | ${[]}
${'selectedAssignees'} | ${''}
`('will be an empty array if given a value of `$value`', ({ value, field }) => {
expect(buildCycleAnalyticsInitialData({ projects: value })).toMatchObject({
[field]: [],
});
});
});
describe('selectedLabels', () => {
it('will be set given an array of labels', () => {
const selectedLabels = ['krillin', 'chiao-tzu'];
expect(
buildCycleAnalyticsInitialData({ labels: JSON.stringify(selectedLabels) }),
).toMatchObject({ selectedLabels });
});
it.each`
field | value
${'selectedLabels'} | ${null}
${'selectedLabels'} | ${[]}
${'selectedLabels'} | ${''}
`('will be an empty array if given a value of `$value`', ({ value, field }) => {
expect(buildCycleAnalyticsInitialData({ projects: value })).toMatchObject({
[field]: [],
});
});
});
describe.each`
field | key | value
${'milestone'} | ${'selectedMilestone'} | ${'cell-saga'}
${'author'} | ${'selectedAuthor'} | ${'cell'}
`('$field', ({ field, value, key }) => {
it(`will set ${key} field with the given value`, () => {
expect(buildCycleAnalyticsInitialData({ [field]: value })).toMatchObject({ [key]: value });
});
it(`will set ${key} to null if omitted`, () => {
expect(buildCycleAnalyticsInitialData()).toMatchObject({ [key]: null });
});
});
describe.each` describe.each`
field | value field | value
${'createdBefore'} | ${'2019-12-31'} ${'createdBefore'} | ${'2019-12-31'}
......
...@@ -9509,6 +9509,12 @@ msgstr "" ...@@ -9509,6 +9509,12 @@ msgstr ""
msgid "Failed to install." msgid "Failed to install."
msgstr "" msgstr ""
msgid "Failed to load assignees. Please try again."
msgstr ""
msgid "Failed to load authors. Please try again."
msgstr ""
msgid "Failed to load emoji list." msgid "Failed to load emoji list."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment