Commit adcaabed authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Persist filters in query params

BE filters should be reflected in
the filter bar on load

Ensure we build the initial state from
the backend supplied params

Fetch initial token data on load

Fetches the token data when the filter
bar component is created
parent e4f60986
......@@ -58,14 +58,6 @@ export default {
type: Boolean,
required: true,
},
milestonesPath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
......@@ -76,6 +68,10 @@ export default {
'selectedGroup',
'selectedProjects',
'selectedStage',
'selectedMilestone',
'selectedAuthor',
'selectedLabels',
'selectedAssignees',
'stages',
'summary',
'currentStageEvents',
......@@ -124,6 +120,10 @@ export default {
'project_ids[]': this.selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
'label_name[]': this.selectedLabels,
'assignee_username[]': this.selectedAssignees,
};
},
stageCount() {
......@@ -135,8 +135,6 @@ export default {
},
mounted() {
const {
labelsPath,
milestonesPath,
glFeatures: {
cycleAnalyticsScatterplotEnabled: hasDurationChart,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian,
......@@ -151,8 +149,6 @@ export default {
hasPathNavigation,
hasFilterBar,
});
this.setPaths({ labelsPath, milestonesPath });
},
methods: {
...mapActions([
......@@ -175,7 +171,6 @@ export default {
'createStage',
'clearFormErrors',
]),
...mapActions('filters', ['setPaths']),
onGroupSelect(group) {
this.setSelectedGroup(group);
this.fetchCycleAnalyticsData();
......
......@@ -100,11 +100,14 @@ export default {
];
},
},
created() {
this.fetchTokenData();
},
mounted() {
this.initializeTokens();
},
methods: {
...mapActions('filters', ['setFilters']),
...mapActions('filters', ['setFilters', 'fetchTokenData']),
initializeTokens() {
const {
selectedMilestone: milestone = null,
......
......@@ -6,15 +6,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
const el = document.querySelector('#js-cycle-analytics-app');
const {
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
hideGroupDropDown,
milestonesPath = '',
labelsPath = '',
} = el.dataset;
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath, hideGroupDropDown } = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore();
store.dispatch('initializeCycleAnalytics', initialData);
......@@ -30,8 +22,6 @@ export default () => {
noDataSvgPath,
noAccessSvgPath,
hideGroupDropDown: parseBoolean(hideGroupDropDown),
milestonesPath,
labelsPath,
},
}),
});
......
......@@ -234,20 +234,27 @@ export const removeStage = ({ dispatch, state }, stageId) => {
.catch(error => dispatch('receiveRemoveStageError', error));
};
export const setSelectedFilters = ({ commit }, filters) =>
export const setSelectedFilters = ({ commit, dispatch }, filters = {}) => {
commit(types.SET_SELECTED_FILTERS, filters);
if (filters?.group?.fullPath) {
return dispatch('fetchCycleAnalyticsData');
}
return Promise.resolve();
};
export const initializeCycleAnalyticsSuccess = ({ commit }) =>
commit(types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS);
export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) => {
commit(types.INITIALIZE_CYCLE_ANALYTICS, initialData);
dispatch('filters/initialize', initialData);
if (initialData?.group?.fullPath) {
return dispatch('fetchCycleAnalyticsData').then(() =>
dispatch('initializeCycleAnalyticsSuccess'),
);
return Promise.resolve()
.then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => dispatch('initializeCycleAnalyticsSuccess'));
}
return dispatch('initializeCycleAnalyticsSuccess');
};
......
......@@ -6,10 +6,12 @@ import * as types from './mutation_types';
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ dispatch, commit }, { milestonesPath = '', labelsPath = '' }) => {
export const setPaths = ({ commit }, { milestonesPath = '', labelsPath = '' }) => {
commit(types.SET_MILESTONES_PATH, appendExtension(milestonesPath));
commit(types.SET_LABELS_PATH, appendExtension(labelsPath));
};
export const fetchTokenData = ({ dispatch }) => {
return Promise.all([
dispatch('fetchLabels'),
dispatch('fetchMilestones'),
......@@ -79,20 +81,8 @@ export const fetchAssignees = ({ commit, rootGetters }, query = '') => {
});
};
export const setFilters = ({ dispatch, state }, params) => {
const { selectedLabels: labelNames = [], ...rest } = params;
const {
labels: { data: labelsList = [] },
} = state;
const selectedLabels = labelsList.filter(({ title }) => labelNames.includes(title));
const nextFilters = {
...rest,
selectedLabels,
};
return dispatch('setSelectedFilters', nextFilters, { root: true });
};
export const setFilters = ({ dispatch }, nextFilters) =>
dispatch('setSelectedFilters', nextFilters, { root: true });
export const initialize = ({ dispatch, commit }, initialFilters) => {
commit(types.INITIALIZE, initialFilters);
......
......@@ -115,5 +115,11 @@ export default {
state.isSavingStageOrder = false;
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 () => ({
selectedGroup: null,
selectedProjects: [],
selectedStage: null,
selectedAuthor: null,
selectedMilestone: null,
selectedAssignees: [],
selectedLabels: [],
currentStageEvents: [],
......
......@@ -79,6 +79,12 @@ export const buildCycleAnalyticsInitialData = ({
groupFullPath = null,
groupParentId = null,
groupAvatarUrl = null,
author = null,
milestone = null,
labels = null,
assignees = null,
labelsPath = '',
milestonesPath = '',
} = {}) => ({
group: groupId
? convertObjectPropsToCamelCase(
......@@ -96,6 +102,12 @@ export const buildCycleAnalyticsInitialData = ({
selectedProjects: projects
? 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') => {
......
......@@ -48,6 +48,18 @@ const defaultStubs = {
GroupsDropdownFilter: true,
};
const initialCycleAnalyticsState = {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
selectedMilestone: null,
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
milestonesPath,
labelsPath,
group: selectedGroup,
};
function createComponent({
opts = {
stubs: defaultStubs,
......@@ -68,8 +80,6 @@ function createComponent({
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
milestonesPath,
labelsPath,
baseStagesEndpoint: mockData.endpoints.baseStagesEndpoint,
hideGroupDropDown,
...props,
......@@ -601,30 +611,31 @@ describe('Cycle Analytics component', () => {
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(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
mock = new MockAdapter(axios);
wrapper = createComponent();
wrapper = createComponent({
shallow: false,
scatterplotEnabled: false,
stubs: {
...defaultStubs,
},
});
return wrapper.vm.$nextTick();
wrapper.vm.$store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
});
it('sets the created_after and created_before url parameters', () => {
return shouldSetUrlParams({
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: null,
'project_ids[]': [],
});
return shouldSetUrlParams(defaultParams);
});
describe('with hideGroupDropDown=true', () => {
......@@ -635,31 +646,23 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios);
wrapper = createComponent({
shallow: false,
scatterplotEnabled: false,
stubs: {
...defaultStubs,
},
props: {
hideGroupDropDown: true,
},
});
wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
...initialCycleAnalyticsState,
group: fakeGroup,
});
return wrapper.vm.$nextTick();
});
it('sets the group_id url parameter', () => {
return shouldSetUrlParams({
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: null,
'project_ids[]': [],
});
});
});
......@@ -669,34 +672,29 @@ describe('Cycle Analytics component', () => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...fakeGroup,
});
return wrapper.vm.$nextTick();
});
it('sets the group_id url parameter', () => {
return shouldSetUrlParams({
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
...defaultParams,
group_id: fakeGroup.fullPath,
'project_ids[]': [],
});
});
});
describe('with a group and selectedProjectIds set', () => {
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...selectedGroup,
});
wrapper.vm.$store.dispatch('setSelectedProjects', mockData.selectedProjects);
return wrapper.vm.$nextTick();
});
it('sets the project_ids url parameter', () => {
return shouldSetUrlParams({
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
......@@ -704,5 +702,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,
});
});
});
});
});
......@@ -18,9 +18,11 @@ describe('Filter bar', () => {
let store;
let setFiltersMock;
let fetchTokenDataMock;
const createStore = (initialState = {}) => {
setFiltersMock = jest.fn();
fetchTokenDataMock = jest.fn();
return new Vuex.Store({
modules: {
......@@ -32,6 +34,7 @@ describe('Filter bar', () => {
},
actions: {
setFilters: setFiltersMock,
fetchTokenData: fetchTokenDataMock,
},
},
},
......@@ -57,13 +60,21 @@ describe('Filter bar', () => {
.props('availableTokens')
.filter(token => token.type === type)[0];
it('renders GlFilteredSearch component', () => {
describe('default', () => {
beforeEach(() => {
store = createStore();
wrapper = createComponent(store);
});
it('renders GlFilteredSearch component', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
it('fetches the token data', () => {
expect(fetchTokenDataMock).toHaveBeenCalled();
});
});
describe('when the state has data', () => {
beforeEach(() => {
store = createStore({
......
......@@ -60,7 +60,7 @@ describe('Cycle analytics actions', () => {
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
testAction(
return testAction(
actions[action],
payload,
state,
......@@ -198,7 +198,7 @@ describe('Cycle analytics actions', () => {
});
it(`dispatches actions for required value stream analytics analytics data`, () => {
testAction(
return testAction(
actions.fetchCycleAnalyticsData,
state,
null,
......@@ -648,9 +648,7 @@ describe('Cycle analytics actions', () => {
});
describe('receiveStageMedianValuesError', () => {
beforeEach(() => {});
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () => {
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () =>
testAction(
actions.receiveStageMedianValuesError,
null,
......@@ -661,8 +659,7 @@ describe('Cycle analytics actions', () => {
},
],
[],
);
});
));
it('will flash an error message', () => {
actions.receiveStageMedianValuesError({ commit: () => {} });
......
......@@ -59,10 +59,7 @@ describe('Filters actions', () => {
[
{
type: 'setSelectedFilters',
payload: {
...nextFilters,
selectedLabels: [],
},
payload: nextFilters,
},
],
);
......@@ -79,7 +76,7 @@ describe('Filters actions', () => {
type: 'setSelectedFilters',
payload: {
...nextFilters,
selectedLabels: [filterLabels[1]],
selectedLabels: [filterLabels[1].title],
},
},
],
......@@ -88,7 +85,7 @@ describe('Filters actions', () => {
});
describe('setPaths', () => {
it('sets the api paths and dispatches requests for initial data', () => {
it('sets the api paths', () => {
return testAction(
actions.setPaths,
{ milestonesPath, labelsPath },
......@@ -97,6 +94,18 @@ describe('Filters actions', () => {
{ payload: 'fake_milestones_path.json', type: types.SET_MILESTONES_PATH },
{ payload: 'fake_labels_path.json', type: types.SET_LABELS_PATH },
],
[],
);
});
});
describe('fetchTokenData', () => {
it('dispatches requests for token data', () => {
return testAction(
actions.fetchTokenData,
{ milestonesPath, labelsPath },
state,
[],
[
{ type: 'fetchLabels' },
{ type: 'fetchMilestones' },
......
......@@ -81,6 +81,12 @@ describe('buildCycleAnalyticsInitialData', () => {
${'createdBefore'} | ${null}
${'createdAfter'} | ${null}
${'selectedProjects'} | ${[]}
${'selectedAuthor'} | ${null}
${'selectedMilestone'} | ${null}
${'selectedLabels'} | ${[]}
${'selectedAssignees'} | ${[]}
${'labelsPath'} | ${''}
${'milestonesPath'} | ${''}
`('will set a default value for "$field" if is not present', ({ field, value }) => {
expect(buildCycleAnalyticsInitialData()).toMatchObject({
[field]: value,
......@@ -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`
field | value
${'createdBefore'} | ${'2019-12-31'}
......
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