Commit e7459ead authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '232465-mlunoe-separate-vsa-base-and-filter-bar-state' into 'master'

Refactor(VSA): Separate query and state management

See merge request gitlab-org/gitlab!39603
parents c12d9498 7db22d0b
<script>
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility';
export default {
props: {
......@@ -14,7 +14,7 @@ export default {
immediate: true,
deep: true,
handler(newQuery) {
historyPushState(setUrlParams(newQuery, window.location.href, true));
historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }));
},
},
},
......
......@@ -65,10 +65,6 @@ export default {
'selectedGroup',
'selectedProjects',
'selectedStage',
'selectedMilestone',
'selectedAuthor',
'selectedLabels',
'selectedAssignees',
'stages',
'currentStageEvents',
'errorCode',
......@@ -123,18 +119,12 @@ export default {
},
query() {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
const selectedLabels = this.selectedLabels?.length ? this.selectedLabels : null;
const selectedAssignees = this.selectedAssignees?.length ? this.selectedAssignees : null;
return {
group_id: !this.hideGroupDropDown ? this.currentGroupPath : null,
'project_ids[]': selectedProjectIds,
project_ids: selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
'label_name[]': selectedLabels,
'assignee_username[]': selectedAssignees,
};
},
stageCount() {
......
<script>
import { mapState, mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
......@@ -28,6 +29,7 @@ export default {
name: 'FilterBar',
components: {
FilteredSearchBar,
UrlSync,
},
props: {
groupPath: {
......@@ -37,11 +39,14 @@ export default {
},
computed: {
...mapState('filters', {
milestones: state => state.milestones.data,
labels: state => state.labels.data,
authors: state => state.authors.data,
assignees: state => state.assignees.data,
initialTokens: state => state.initialTokens,
selectedMilestone: state => state.milestones.selected,
selectedAuthor: state => state.authors.selected,
selectedLabels: state => state.labels.selected,
selectedAssignees: state => state.assignees.selected,
milestonesData: state => state.milestones.data,
labelsData: state => state.labels.data,
authorsData: state => state.authors.data,
assigneesData: state => state.assignees.data,
}),
tokens() {
return [
......@@ -50,7 +55,7 @@ export default {
title: __('Milestone'),
type: 'milestone',
token: MilestoneToken,
initialMilestones: this.milestones,
initialMilestones: this.milestonesData,
unique: true,
symbol: '%',
operators: [{ value: '=', description: 'is', default: 'true' }],
......@@ -61,7 +66,7 @@ export default {
title: __('Label'),
type: 'labels',
token: LabelToken,
initialLabels: this.labels,
initialLabels: this.labelsData,
unique: false,
symbol: '~',
operators: [{ value: '=', description: 'is', default: 'true' }],
......@@ -72,7 +77,7 @@ export default {
title: __('Author'),
type: 'author',
token: AuthorToken,
initialAuthors: this.authors,
initialAuthors: this.authorsData,
unique: true,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchAuthors: this.fetchAuthors,
......@@ -82,13 +87,24 @@ export default {
title: __('Assignees'),
type: 'assignees',
token: AuthorToken,
initialAuthors: this.assignees,
initialAuthors: this.assigneesData,
unique: false,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchAuthors: this.fetchAssignees,
},
];
},
query() {
const selectedLabels = this.selectedLabels?.length ? this.selectedLabels : null;
const selectedAssignees = this.selectedAssignees?.length ? this.selectedAssignees : null;
return {
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
label_name: selectedLabels,
assignee_username: selectedAssignees,
};
},
},
methods: {
...mapActions('filters', [
......@@ -104,8 +120,7 @@ export default {
selectedAuthor: author = null,
selectedAssignees: assignees = [],
selectedLabels: labels = [],
} = this.initialTokens;
} = this;
return prepareTokens({ milestone, author, assignees, labels });
},
processFilters(filters) {
......@@ -130,7 +145,6 @@ export default {
return acc;
}, {});
},
handleFilter(filters) {
const { labels, milestone, author, assignees } = this.processFilters(filters);
......@@ -146,12 +160,16 @@ export default {
</script>
<template>
<filtered-search-bar
:namespace="groupPath"
recent-searches-storage-key="value-stream-analytics"
:search-input-placeholder="__('Filter results')"
:tokens="tokens"
:initial-filter-value="initialFilterValue()"
@onFilter="handleFilter"
/>
<div>
<filtered-search-bar
class="gl-flex-grow-1"
:namespace="groupPath"
recent-searches-storage-key="value-stream-analytics"
:search-input-placeholder="__('Filter results')"
:tokens="tokens"
:initial-filter-value="initialFilterValue()"
@onFilter="handleFilter"
/>
<url-sync :query="query" />
</div>
</template>
......@@ -5,6 +5,21 @@ import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../utils';
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ dispatch }, options) => {
const { groupPath = '', milestonesPath = '', labelsPath = '' } = options;
// TODO: After we remove instance VSA we can rely on the paths from the BE
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
const milestonesEndpoint = milestonesPath || `/groups/${groupPath}/-/milestones`;
const labelsEndpoint = labelsPath || `/groups/${groupPath}/-/labels`;
return dispatch('filters/setEndpoints', {
labelsEndpoint: appendExtension(labelsEndpoint),
milestonesEndpoint: appendExtension(milestonesEndpoint),
});
};
export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags);
......@@ -237,20 +252,33 @@ export const removeStage = ({ dispatch, getters }, stageId) => {
.catch(error => dispatch('receiveRemoveStageError', error));
};
export const setSelectedFilters = ({ commit }, filters = {}) =>
commit(types.SET_SELECTED_FILTERS, filters);
export const initializeCycleAnalyticsSuccess = ({ commit }) =>
commit(types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS);
export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) => {
commit(types.INITIALIZE_CYCLE_ANALYTICS, initialData);
const { featureFlags = {} } = initialData;
const {
featureFlags = {},
milestonesPath,
labelsPath,
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
} = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags);
if (initialData.group?.fullPath) {
return dispatch('filters/initialize', { groupPath: initialData.group.fullPath, ...initialData })
return Promise.all([
dispatch('setPaths', { groupPath: initialData.group.fullPath, milestonesPath, labelsPath }),
dispatch('filters/initialize', {
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
}),
])
.then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => dispatch('initializeCycleAnalyticsSuccess'));
}
......@@ -336,3 +364,7 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
}
return dispatch('fetchValueStreamData');
};
export const setFilters = ({ dispatch }) => {
return dispatch('fetchCycleAnalyticsData');
};
......@@ -10,34 +10,36 @@ export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDE
export const currentValueStreamId = ({ selectedValueStream }) =>
selectedValueStream?.id || DEFAULT_VALUE_STREAM_ID;
export const currentGroupPath = ({ selectedGroup }) =>
selectedGroup && selectedGroup.fullPath ? selectedGroup.fullPath : null;
export const currentGroupPath = ({ selectedGroup }) => selectedGroup?.fullPath || null;
export const currentGroupParentPath = ({ selectedGroup }, getters) =>
selectedGroup?.parentId || getters.currentGroupPath;
export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects.length ? selectedProjects.map(({ id }) => id) : [];
selectedProjects?.map(({ id }) => id) || [];
export const cycleAnalyticsRequestParams = (
{
export const cycleAnalyticsRequestParams = (state, getters) => {
const {
startDate = null,
endDate = null,
selectedAuthor = null,
selectedMilestone = null,
selectedAssignees = [],
selectedLabels = [],
},
getters,
) => ({
project_ids: getters.selectedProjectIds,
created_after: startDate ? dateFormat(startDate, dateFormats.isoDate) : null,
created_before: endDate ? dateFormat(endDate, dateFormats.isoDate) : null,
author_username: selectedAuthor,
milestone_title: selectedMilestone,
assignee_username: selectedAssignees,
label_name: selectedLabels,
});
filters: {
authors: { selected: selectedAuthor = null },
milestones: { selected: selectedMilestone = null },
assignees: { selected: selectedAssignees = [] },
labels: { selected: selectedLabels = [] },
},
} = state;
return {
project_ids: getters.selectedProjectIds,
created_after: startDate ? dateFormat(startDate, dateFormats.isoDate) : null,
created_before: endDate ? dateFormat(endDate, dateFormats.isoDate) : null,
author_username: selectedAuthor,
milestone_title: selectedMilestone,
assignee_username: selectedAssignees,
label_name: selectedLabels,
};
};
const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
......
......@@ -4,23 +4,17 @@ import { __ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types';
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
// TODO: After we remove instance VSA we can rely on the paths from the BE
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
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 setEndpoints = ({ commit }, { milestonesEndpoint, labelsEndpoint }) => {
commit(types.SET_MILESTONES_ENDPOINT, milestonesEndpoint);
commit(types.SET_LABELS_ENDPOINT, labelsEndpoint);
};
export const fetchMilestones = ({ commit, state }, search_title = '') => {
commit(types.REQUEST_MILESTONES);
const { milestonesPath } = state;
const { milestonesEndpoint } = state;
return axios
.get(milestonesPath, { params: { search_title } })
.get(milestonesEndpoint, { params: { search_title } })
.then(response => {
commit(types.RECEIVE_MILESTONES_SUCCESS, response.data);
return response;
......@@ -36,7 +30,7 @@ export const fetchLabels = ({ commit, state }, search = '') => {
commit(types.REQUEST_LABELS);
return axios
.get(state.labelsPath, { params: { search } })
.get(state.labelsEndpoint, { params: { search } })
.then(response => {
commit(types.RECEIVE_LABELS_SUCCESS, response.data);
return response;
......@@ -85,15 +79,12 @@ export const fetchAssignees = ({ commit, rootGetters }, query = '') => {
});
};
export const setFilters = ({ dispatch }, nextFilters) => {
return Promise.resolve()
.then(() => dispatch('setSelectedFilters', nextFilters, { root: true }))
.then(() => dispatch('fetchCycleAnalyticsData', null, { root: true }));
export const setFilters = ({ commit, dispatch }, filters) => {
commit(types.SET_SELECTED_FILTERS, filters);
return dispatch('setFilters', filters, { root: true });
};
export const initialize = ({ dispatch, commit }, initialFilters) => {
commit(types.INITIALIZE, initialFilters);
return dispatch('setPaths', initialFilters).then(() =>
dispatch('setSelectedFilters', initialFilters, { root: true }),
);
export const initialize = ({ commit }, initialFilters) => {
commit(types.SET_SELECTED_FILTERS, initialFilters);
};
export const INITIALIZE = 'INITIALIZE';
export const SET_MILESTONES_PATH = 'SET_MILESTONES_PATH';
export const SET_LABELS_PATH = 'SET_LABELS_PATH';
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';
......@@ -17,3 +16,5 @@ 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';
export const SET_SELECTED_FILTERS = 'SET_SELECTED_FILTERS';
import * as types from './mutation_types';
export default {
[types.INITIALIZE](
state,
{
[types.SET_SELECTED_FILTERS](state, params) {
const {
selectedAuthor = null,
selectedMilestone = null,
selectedAssignees = [],
selectedLabels = [],
} = {},
) {
state.initialTokens = {
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
};
} = params;
state.authors.selected = selectedAuthor;
state.assignees.selected = selectedAssignees;
state.milestones.selected = selectedMilestone;
state.labels.selected = selectedLabels;
},
[types.SET_MILESTONES_PATH](state, milestonesPath) {
state.milestonesPath = milestonesPath;
[types.SET_MILESTONES_ENDPOINT](state, milestonesEndpoint) {
state.milestonesEndpoint = milestonesEndpoint;
},
[types.SET_LABELS_PATH](state, labelsPath) {
state.labelsPath = labelsPath;
[types.SET_LABELS_ENDPOINT](state, labelsEndpoint) {
state.labelsEndpoint = labelsEndpoint;
},
[types.REQUEST_MILESTONES](state) {
state.milestones.isLoading = true;
......
export default () => ({
milestonesPath: '',
labelsPath: '',
milestonesEndpoint: '',
labelsEndpoint: '',
milestones: {
isLoading: false,
data: [],
selected: null,
},
labels: {
isLoading: false,
data: [],
selected: [],
},
authors: {
isLoading: false,
data: [],
selected: null,
},
assignees: {
isLoading: false,
data: [],
},
initialTokens: {
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
selected: [],
},
});
......@@ -4,7 +4,6 @@ export const SET_SELECTED_GROUP = 'SET_SELECTED_GROUP';
export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const SET_SELECTED_FILTERS = 'SET_SELECTED_FILTERS';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
......
......@@ -115,13 +115,6 @@ export default {
state.isSavingStageOrder = false;
state.errorSavingStageOrder = true;
},
[types.SET_SELECTED_FILTERS](state, params) {
const { selectedAuthor, selectedAssignees, selectedMilestone, selectedLabels } = params;
state.selectedAuthor = selectedAuthor;
state.selectedAssignees = selectedAssignees;
state.selectedMilestone = selectedMilestone;
state.selectedLabels = selectedLabels;
},
[types.REQUEST_CREATE_VALUE_STREAM](state) {
state.isCreatingValueStream = true;
state.createValueStreamErrors = {};
......
......@@ -16,10 +16,6 @@ export default () => ({
selectedGroup: null,
selectedProjects: [],
selectedStage: null,
selectedAuthor: null,
selectedMilestone: null,
selectedAssignees: [],
selectedLabels: [],
selectedValueStream: null,
currentStageEvents: [],
......
......@@ -20,8 +20,8 @@ import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_wo
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { toYmd } from 'ee/analytics/shared/utils';
import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
import httpStatusCodes from '~/lib/utils/http_status';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as mockData from '../mock_data';
......@@ -45,6 +45,7 @@ const defaultStubs = {
GroupsDropdownFilter: true,
ValueStreamSelect: true,
Metrics: true,
UrlSync,
};
const defaultFeatureFlags = {
......@@ -57,10 +58,6 @@ const defaultFeatureFlags = {
const initialCycleAnalyticsState = {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
group: selectedGroup,
};
......@@ -84,7 +81,6 @@ function createComponent({
const comp = func(Component, {
localVue,
store,
mixins: [UrlSyncMixin],
propsData: {
emptyStateSvgPath,
noDataSvgPath,
......@@ -124,6 +120,14 @@ function createComponent({
return comp;
}
async function shouldMergeUrlParams(wrapper, result) {
await wrapper.vm.$nextTick();
expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, {
spreadArrays: true,
});
expect(commonUtils.historyPushState).toHaveBeenCalled();
}
describe('Cycle Analytics component', () => {
let wrapper;
let mock;
......@@ -134,13 +138,6 @@ describe('Cycle Analytics component', () => {
.findAll(StageNavItem)
.at(index);
const shouldSetUrlParams = result => {
return wrapper.vm.$nextTick().then(() => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith(result, window.location.href, true);
expect(commonUtils.historyPushState).toHaveBeenCalled();
});
};
const displaysProjectsDropdownFilter = flag => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(flag);
};
......@@ -650,18 +647,14 @@ describe('Cycle Analytics component', () => {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
'project_ids[]': null,
milestone_title: null,
author_username: null,
'assignee_username[]': null,
'label_name[]': null,
project_ids: null,
};
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
wrapper = createComponent();
......@@ -670,13 +663,13 @@ describe('Cycle Analytics component', () => {
});
it('sets the created_after and created_before url parameters', () => {
return shouldSetUrlParams(defaultParams);
return shouldMergeUrlParams(wrapper, defaultParams);
});
describe('with hideGroupDropDown=true', () => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
......@@ -693,7 +686,7 @@ describe('Cycle Analytics component', () => {
});
it('sets the group_id url parameter', () => {
return shouldSetUrlParams({
return shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
......@@ -710,7 +703,7 @@ describe('Cycle Analytics component', () => {
});
it('sets the group_id url parameter', () => {
return shouldSetUrlParams({
return shouldMergeUrlParams(wrapper, {
...defaultParams,
group_id: fakeGroup.fullPath,
});
......@@ -728,35 +721,12 @@ describe('Cycle Analytics component', () => {
});
it('sets the project_ids url parameter', () => {
return shouldSetUrlParams({
return shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
'project_ids[]': selectedProjectIds,
});
});
});
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,
project_ids: selectedProjectIds,
});
});
});
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import storeConfig from 'ee/analytics/cycle_analytics/store';
import FilterBar, { prepareTokens } from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { filterMilestones, filterLabels } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -13,9 +19,32 @@ const labelsTokenType = 'labels';
const authorTokenType = 'author';
const assigneesTokenType = 'assignees';
const initialFilterBarState = {
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: null,
selectedLabels: null,
};
const defaultParams = {
milestone_title: null,
author_username: null,
assignee_username: null,
label_name: null,
};
async function shouldMergeUrlParams(wrapper, result) {
await wrapper.vm.$nextTick();
expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, {
spreadArrays: true,
});
expect(commonUtils.historyPushState).toHaveBeenCalled();
}
describe('Filter bar', () => {
let wrapper;
let store;
let mock;
let setFiltersMock;
......@@ -38,21 +67,30 @@ describe('Filter bar', () => {
});
};
const createComponent = initialStore =>
shallowMount(FilterBar, {
const createComponent = initialStore => {
return shallowMount(FilterBar, {
localVue,
store: initialStore,
propsData: {
groupPath: 'foo',
},
stubs: {
UrlSync,
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
const selectedMilestone = [filterMilestones[0]];
const selectedLabel = [filterLabels[0]];
const selectedLabels = [filterLabels[0]];
const findFilteredSearch = () => wrapper.find(FilteredSearchBar);
const getSearchToken = type =>
......@@ -75,7 +113,7 @@ describe('Filter bar', () => {
beforeEach(() => {
store = createStore({
milestones: { data: selectedMilestone },
labels: { data: selectedLabel },
labels: { data: selectedLabels },
authors: { data: [] },
assignees: { data: [] },
});
......@@ -101,7 +139,7 @@ describe('Filter bar', () => {
it('provides the initial label token', () => {
const { initialLabels: labelToken } = getSearchToken(labelsTokenType);
expect(labelToken).toHaveLength(selectedLabel.length);
expect(labelToken).toHaveLength(selectedLabels.length);
});
});
......@@ -117,13 +155,13 @@ describe('Filter bar', () => {
it('clicks on the search button, setFilters is dispatched', () => {
findFilteredSearch().vm.$emit('onFilter', [
{ type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
{ type: 'labels', value: { data: selectedLabel[0].title, operator: '=' } },
{ type: 'labels', value: { data: selectedLabels[0].title, operator: '=' } },
]);
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedLabels: [selectedLabel[0].title],
selectedLabels: [selectedLabels[0].title],
selectedMilestone: selectedMilestone[0].title,
selectedAssignees: [],
selectedAuthor: null,
......@@ -206,4 +244,31 @@ describe('Filter bar', () => {
expect(res).toEqual(result);
});
});
describe.each`
stateKey | payload | paramKey
${'selectedMilestone'} | ${'12.0'} | ${'milestone_title'}
${'selectedAuthor'} | ${'rootUser'} | ${'author_username'}
${'selectedLabels'} | ${['Afternix', 'Brouceforge']} | ${'label_name'}
${'selectedAssignees'} | ${['rootUser', 'secondaryUser']} | ${'assignee_username'}
`('with a $stateKey updates the $paramKey url parameter', ({ stateKey, payload, paramKey }) => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
wrapper = createComponent(storeConfig);
wrapper.vm.$store.dispatch('filters/setFilters', {
...initialFilterBarState,
[stateKey]: payload,
});
});
it(`sets the ${paramKey} url parameter`, () => {
return shouldMergeUrlParams(wrapper, {
...defaultParams,
[paramKey]: payload,
});
});
});
});
......@@ -16,6 +16,10 @@ import {
valueStreams,
} from '../mock_data';
const groupPath = 'fake_group_path';
const milestonesPath = 'fake_milestones_path';
const labelsPath = 'fake_labels_path';
const stageData = { events: [] };
const error = new Error(`Request failed with status code ${httpStatusCodes.NOT_FOUND}`);
const flashErrorMessage = 'There was an error while fetching value stream analytics data.';
......@@ -102,6 +106,48 @@ describe('Cycle analytics actions', () => {
});
});
describe('setPaths', () => {
describe('with endpoint paths provided', () => {
it('dispatches the filters/setEndpoints action', () => {
return testAction(
actions.setPaths,
{ groupPath, milestonesPath, labelsPath },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
labelsEndpoint: 'fake_labels_path.json',
milestonesEndpoint: 'fake_milestones_path.json',
},
},
],
);
});
});
describe('without endpoint paths provided', () => {
it('dispatches the filters/setEndpoints action', () => {
return testAction(
actions.setPaths,
{ groupPath },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
labelsEndpoint: '/groups/fake_group_path/-/labels.json',
milestonesEndpoint: '/groups/fake_group_path/-/milestones.json',
},
},
],
);
});
});
});
describe('setDateRange', () => {
const payload = { startDate, endDate };
......@@ -1047,4 +1093,10 @@ describe('Cycle analytics actions', () => {
);
});
});
describe('setFilters', () => {
it('dispatches the fetchCycleAnalyticsData action', () => {
return testAction(actions.setFilters, null, state, [], [{ type: 'fetchCycleAnalyticsData' }]);
});
});
});
......@@ -121,10 +121,12 @@ describe('Cycle analytics getters', () => {
startDate,
endDate,
selectedProjects,
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
filters: {
authors: { selected: selectedAuthor },
milestones: { selected: selectedMilestone },
assignees: { selected: selectedAssignees },
labels: { selected: selectedLabels },
},
};
});
......
......@@ -8,8 +8,8 @@ import httpStatusCodes from '~/lib/utils/http_status';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { filterMilestones, filterUsers, filterLabels } from '../../../mock_data';
const milestonesPath = 'fake_milestones_path';
const labelsPath = 'fake_labels_path';
const milestonesEndpoint = 'fake_milestones_endpoint';
const labelsEndpoint = 'fake_labels_endpoint';
jest.mock('~/flash');
......@@ -33,44 +33,35 @@ describe('Filters actions', () => {
describe('initialize', () => {
const initialData = {
milestonesPath,
labelsPath,
milestonesEndpoint,
labelsEndpoint,
selectedAuthor: 'Mr cool',
selectedMilestone: 'NEXT',
};
it('dispatches setPaths, setSelectedFilters', () => {
return actions
.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
)
.then(() => {
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledWith('setPaths', initialData);
expect(mockDispatch).toHaveBeenCalledWith('setSelectedFilters', initialData, {
root: true,
});
});
it('does not dispatch', () => {
const result = actions.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
);
expect(result).toBeUndefined();
expect(mockDispatch).not.toHaveBeenCalled();
});
it(`commits the ${types.INITIALIZE}`, () => {
return actions
.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
)
.then(() => {
expect(mockCommit).toHaveBeenCalledWith(types.INITIALIZE, initialData);
});
it(`commits the ${types.SET_SELECTED_FILTERS}`, () => {
actions.initialize(
{
state,
dispatch: mockDispatch,
commit: mockCommit,
},
initialData,
);
expect(mockCommit).toHaveBeenCalledWith(types.SET_SELECTED_FILTERS, initialData);
});
});
......@@ -80,57 +71,36 @@ describe('Filters actions', () => {
selectedMilestone: 'NEXT',
};
it('dispatches the root/setSelectedFilters and root/fetchCycleAnalyticsData actions', () => {
it('dispatches the root/setFilters action', () => {
return testAction(
actions.setFilters,
nextFilters,
state,
[],
[
{
type: 'setSelectedFilters',
payload: nextFilters,
},
{
type: 'fetchCycleAnalyticsData',
payload: null,
type: types.SET_SELECTED_FILTERS,
},
],
);
});
it('sets the selectedLabels from the labels available', () => {
return testAction(
actions.setFilters,
{ ...nextFilters, selectedLabels: [filterLabels[1].title] },
{ ...state, labels: { data: filterLabels } },
[],
[
{
type: 'setSelectedFilters',
payload: {
...nextFilters,
selectedLabels: [filterLabels[1].title],
},
},
{
type: 'fetchCycleAnalyticsData',
payload: null,
type: 'setFilters',
payload: nextFilters,
},
],
);
});
});
describe('setPaths', () => {
describe('setEndpoints', () => {
it('sets the api paths', () => {
return testAction(
actions.setPaths,
{ milestonesPath, labelsPath },
actions.setEndpoints,
{ milestonesEndpoint, labelsEndpoint },
state,
[
{ payload: 'fake_milestones_path.json', type: types.SET_MILESTONES_PATH },
{ payload: 'fake_labels_path.json', type: types.SET_LABELS_PATH },
{ payload: 'fake_milestones_endpoint', type: types.SET_MILESTONES_ENDPOINT },
{ payload: 'fake_labels_endpoint', type: types.SET_LABELS_ENDPOINT },
],
[],
);
......@@ -185,14 +155,14 @@ describe('Filters actions', () => {
describe('fetchMilestones', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(milestonesPath).replyOnce(httpStatusCodes.OK, filterMilestones);
mock.onGet(milestonesEndpoint).replyOnce(httpStatusCodes.OK, filterMilestones);
});
it('dispatches RECEIVE_MILESTONES_SUCCESS with received data', () => {
return testAction(
actions.fetchMilestones,
null,
{ ...state, milestonesPath },
{ ...state, milestonesEndpoint },
[
{ type: types.REQUEST_MILESTONES },
{ type: types.RECEIVE_MILESTONES_SUCCESS, payload: filterMilestones },
......@@ -237,7 +207,7 @@ describe('Filters actions', () => {
return testAction(
actions.fetchAssignees,
null,
{ ...state, milestonesPath },
{ ...state, milestonesEndpoint },
[
{ type: types.REQUEST_ASSIGNEES },
{ type: types.RECEIVE_ASSIGNEES_SUCCESS, payload: filterUsers },
......@@ -275,14 +245,14 @@ describe('Filters actions', () => {
describe('fetchLabels', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(labelsPath).replyOnce(httpStatusCodes.OK, filterLabels);
mock.onGet(labelsEndpoint).replyOnce(httpStatusCodes.OK, filterLabels);
});
it('dispatches RECEIVE_LABELS_SUCCESS with received data', () => {
return testAction(
actions.fetchLabels,
null,
{ ...state, labelsPath },
{ ...state, labelsEndpoint },
[
{ type: types.REQUEST_LABELS },
{ type: types.RECEIVE_LABELS_SUCCESS, payload: filterLabels },
......
......@@ -11,7 +11,12 @@ const labels = filterLabels.map(convertObjectPropsToCamelCase);
describe('Filters mutations', () => {
beforeEach(() => {
state = { initialTokens: {}, milestones: {}, authors: {}, labels: {}, assignees: {} };
state = {
authors: { selected: null },
milestones: { selected: null },
assignees: { selected: [] },
labels: { selected: [] },
};
});
afterEach(() => {
......@@ -19,9 +24,9 @@ describe('Filters mutations', () => {
});
it.each`
mutation | stateKey | value
${types.SET_MILESTONES_PATH} | ${'milestonesPath'} | ${'new-milestone-path'}
${types.SET_LABELS_PATH} | ${'labelsPath'} | ${'new-label-path'}
mutation | stateKey | value
${types.SET_MILESTONES_ENDPOINT} | ${'milestonesEndpoint'} | ${'new-milestone-endpoint'}
${types.SET_LABELS_ENDPOINT} | ${'labelsEndpoint'} | ${'new-label-endpoint'}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, value);
......@@ -29,15 +34,15 @@ describe('Filters mutations', () => {
});
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);
mutation | stateKey | value
${types.SET_SELECTED_FILTERS} | ${'authors'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'milestones'} | ${null}
${types.SET_SELECTED_FILTERS} | ${'assignees'} | ${[]}
${types.SET_SELECTED_FILTERS} | ${'labels'} | ${[]}
`('$mutation will set $stateKey with a given value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, { [stateKey]: { selected: value } });
expect(state[rootKey][stateKey]).toEqual(value);
expect(state[stateKey].selected).toEqual(value);
});
it.each`
......
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import UrlSyncComponent from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(val => `urlParams: ${val}`),
mergeUrlParams: jest.fn((query, url) => `urlParams: ${query} ${url}`),
}));
jest.mock('~/lib/utils/common_utils', () => ({
......@@ -27,12 +27,12 @@ describe('url sync component', () => {
};
function expectUrlSync(query) {
expect(setUrlParams).toHaveBeenCalledTimes(1);
expect(setUrlParams).toHaveBeenCalledWith(query, TEST_HOST, true);
expect(mergeUrlParams).toHaveBeenCalledTimes(1);
expect(mergeUrlParams).toHaveBeenCalledWith(query, TEST_HOST, { spreadArrays: true });
const setUrlParamsReturnValue = setUrlParams.mock.results[0].value;
const mergeUrlParamsReturnValue = mergeUrlParams.mock.results[0].value;
expect(historyPushState).toHaveBeenCalledTimes(1);
expect(historyPushState).toHaveBeenCalledWith(setUrlParamsReturnValue);
expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
}
beforeEach(() => {
......@@ -44,7 +44,7 @@ describe('url sync component', () => {
describe('when the query is modified', () => {
const newQuery = { foo: true };
beforeEach(() => {
setUrlParams.mockClear();
mergeUrlParams.mockClear();
historyPushState.mockClear();
wrapper.setProps({ query: newQuery });
});
......
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