Commit c8a39e1a authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch '229489-fe-multiple-value-streams-deep-link-to-selected-value-stream' into 'master'

[FE] Deep link to selected value stream

See merge request gitlab-org/gitlab!42193
parents 36f6fb2d 306c153f
...@@ -65,6 +65,7 @@ export default { ...@@ -65,6 +65,7 @@ export default {
'medians', 'medians',
'isLoadingValueStreams', 'isLoadingValueStreams',
'selectedStageError', 'selectedStageError',
'selectedValueStream',
]), ]),
// NOTE: formEvents are fetched in the same request as the list of stages (fetchGroupStagesAndEvents) // NOTE: formEvents are fetched in the same request as the list of stages (fetchGroupStagesAndEvents)
// so i think its ok to bind formEvents here even though its only used as a prop to the custom-stage-form // so i think its ok to bind formEvents here even though its only used as a prop to the custom-stage-form
...@@ -106,6 +107,7 @@ export default { ...@@ -106,6 +107,7 @@ export default {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null; const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
return { return {
value_stream_id: this.selectedValueStream?.id || null,
project_ids: selectedProjectIds, project_ids: selectedProjectIds,
created_after: toYmd(this.startDate), created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate), created_before: toYmd(this.endDate),
......
...@@ -129,8 +129,8 @@ export default { ...@@ -129,8 +129,8 @@ export default {
isSelected(id) { isSelected(id) {
return Boolean(this.selectedValueStreamId && this.selectedValueStreamId === id); return Boolean(this.selectedValueStreamId && this.selectedValueStreamId === id);
}, },
onSelect(id) { onSelect(selectedId) {
this.setSelectedValueStream(id); this.setSelectedValueStream(this.data.find(({ id }) => id === selectedId));
}, },
onDelete() { onDelete() {
const name = this.selectedValueStreamName; const name = this.selectedValueStreamName;
......
...@@ -61,3 +61,5 @@ export const OVERVIEW_METRICS = { ...@@ -61,3 +61,5 @@ export const OVERVIEW_METRICS = {
TIME_SUMMARY: 'TIME_SUMMARY', TIME_SUMMARY: 'TIME_SUMMARY',
RECENT_ACTIVITY: 'RECENT_ACTIVITY', RECENT_ACTIVITY: 'RECENT_ACTIVITY',
}; };
export const FETCH_VALUE_STREAM_DATA = 'fetchValueStreamData';
...@@ -3,6 +3,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; ...@@ -3,6 +3,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { FETCH_VALUE_STREAM_DATA } from '../constants';
import { import {
removeFlash, removeFlash,
throwIfUserForbidden, throwIfUserForbidden,
...@@ -373,16 +374,19 @@ export const fetchValueStreamData = ({ dispatch }) => ...@@ -373,16 +374,19 @@ export const fetchValueStreamData = ({ dispatch }) =>
export const setSelectedValueStream = ({ commit, dispatch }, streamId) => { export const setSelectedValueStream = ({ commit, dispatch }, streamId) => {
commit(types.SET_SELECTED_VALUE_STREAM, streamId); commit(types.SET_SELECTED_VALUE_STREAM, streamId);
return dispatch('fetchValueStreamData'); return dispatch(FETCH_VALUE_STREAM_DATA);
}; };
export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { export const receiveValueStreamsSuccess = (
{ state: { selectedValueStream = null }, commit, dispatch },
data = [],
) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data); commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
if (data.length) { if (!selectedValueStream && data.length) {
const [firstStream] = data; const [firstStream] = data;
return dispatch('setSelectedValueStream', firstStream.id); return dispatch('setSelectedValueStream', firstStream);
} }
return Promise.resolve(); return dispatch(FETCH_VALUE_STREAM_DATA);
}; };
export const fetchValueStreams = ({ commit, dispatch, getters, state }) => { export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
...@@ -404,7 +408,7 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => { ...@@ -404,7 +408,7 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
throw error; throw error;
}); });
} }
return dispatch('fetchValueStreamData'); return dispatch(FETCH_VALUE_STREAM_DATA);
}; };
export const setFilters = ({ dispatch }) => { export const setFilters = ({ dispatch }) => {
......
...@@ -91,11 +91,13 @@ export default { ...@@ -91,11 +91,13 @@ export default {
createdAfter: startDate = null, createdAfter: startDate = null,
createdBefore: endDate = null, createdBefore: endDate = null,
selectedProjects = [], selectedProjects = [],
selectedValueStream = {},
} = {}, } = {},
) { ) {
state.isLoading = true; state.isLoading = true;
state.currentGroup = group; state.currentGroup = group;
state.selectedProjects = selectedProjects; state.selectedProjects = selectedProjects;
state.selectedValueStream = selectedValueStream;
state.startDate = startDate; state.startDate = startDate;
state.endDate = endDate; state.endDate = endDate;
}, },
...@@ -138,8 +140,8 @@ export default { ...@@ -138,8 +140,8 @@ export default {
state.isDeletingValueStream = false; state.isDeletingValueStream = false;
state.deleteValueStreamError = null; state.deleteValueStreamError = null;
}, },
[types.SET_SELECTED_VALUE_STREAM](state, streamId) { [types.SET_SELECTED_VALUE_STREAM](state, valueStream) {
state.selectedValueStream = state.valueStreams?.find(({ id }) => id === streamId) || null; state.selectedValueStream = valueStream;
}, },
[types.REQUEST_VALUE_STREAMS](state) { [types.REQUEST_VALUE_STREAMS](state) {
state.isLoadingValueStreams = true; state.isLoadingValueStreams = true;
......
...@@ -10,6 +10,17 @@ export default { ...@@ -10,6 +10,17 @@ export default {
export const formattedDate = d => dateFormat(d, dateFormats.defaultDate); export const formattedDate = d => dateFormat(d, dateFormats.defaultDate);
/**
* Creates a value stream object from a dataset. Returns null if no valueStreamId is present.
*
* @param {Object} dataset - The raw value stream object
* @returns {Object} - A value stream object
*/
export const buildValueStreamFromJson = valueStream => {
const { id, name, is_custom: isCustom } = valueStream ? JSON.parse(valueStream) : {};
return id ? { id, name, isCustom } : null;
};
/** /**
* Creates a group object from a dataset. Returns null if no groupId is present. * Creates a group object from a dataset. Returns null if no groupId is present.
* *
...@@ -71,6 +82,7 @@ const buildProjectsFromJSON = (projects = []) => { ...@@ -71,6 +82,7 @@ const buildProjectsFromJSON = (projects = []) => {
* @returns {Object} - The initial data to load the app with * @returns {Object} - The initial data to load the app with
*/ */
export const buildCycleAnalyticsInitialData = ({ export const buildCycleAnalyticsInitialData = ({
valueStream = null,
groupId = null, groupId = null,
createdBefore = null, createdBefore = null,
createdAfter = null, createdAfter = null,
...@@ -82,6 +94,7 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -82,6 +94,7 @@ export const buildCycleAnalyticsInitialData = ({
labelsPath = '', labelsPath = '',
milestonesPath = '', milestonesPath = '',
} = {}) => ({ } = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId group: groupId
? convertObjectPropsToCamelCase( ? convertObjectPropsToCamelCase(
buildGroupFromDataset({ buildGroupFromDataset({
......
...@@ -69,7 +69,7 @@ module Gitlab ...@@ -69,7 +69,7 @@ module Gitlab
def to_data_attributes def to_data_attributes
{}.tap do |attrs| {}.tap do |attrs|
attrs[:group] = group_data_attributes if group attrs[:group] = group_data_attributes if group
attrs[:value_stream] = value_stream_data_attributes if value_stream attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream
attrs[:created_after] = created_after.to_date.iso8601 attrs[:created_after] = created_after.to_date.iso8601
attrs[:created_before] = created_before.to_date.iso8601 attrs[:created_before] = created_before.to_date.iso8601
attrs[:projects] = group_projects(project_ids) if group && project_ids.present? attrs[:projects] = group_projects(project_ids) if group && project_ids.present?
...@@ -94,7 +94,9 @@ module Gitlab ...@@ -94,7 +94,9 @@ module Gitlab
def value_stream_data_attributes def value_stream_data_attributes
{ {
id: value_stream.id id: value_stream.id,
name: value_stream.name,
is_custom: value_stream.custom?
} }
end end
......
...@@ -52,7 +52,10 @@ const defaultFeatureFlags = { ...@@ -52,7 +52,10 @@ const defaultFeatureFlags = {
hasCreateMultipleValueStreams: false, hasCreateMultipleValueStreams: false,
}; };
const [selectedValueStream] = mockData.valueStreams;
const initialCycleAnalyticsState = { const initialCycleAnalyticsState = {
selectedValueStream,
createdAfter: mockData.startDate, createdAfter: mockData.startDate,
createdBefore: mockData.endDate, createdBefore: mockData.endDate,
group: currentGroup, group: currentGroup,
...@@ -627,6 +630,7 @@ describe('Cycle Analytics component', () => { ...@@ -627,6 +630,7 @@ describe('Cycle Analytics component', () => {
describe('Url parameters', () => { describe('Url parameters', () => {
const defaultParams = { const defaultParams = {
value_stream_id: selectedValueStream.id,
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
project_ids: null, project_ids: null,
...@@ -640,9 +644,6 @@ describe('Cycle Analytics component', () => { ...@@ -640,9 +644,6 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockRequiredRoutes(mock); mockRequiredRoutes(mock);
wrapper = await createComponent();
await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
}); });
afterEach(() => { afterEach(() => {
...@@ -651,12 +652,41 @@ describe('Cycle Analytics component', () => { ...@@ -651,12 +652,41 @@ describe('Cycle Analytics component', () => {
wrapper = null; wrapper = null;
}); });
it('sets the created_after and created_before url parameters', async () => { describe('with minimal parameters set set', () => {
await shouldMergeUrlParams(wrapper, defaultParams); beforeEach(async () => {
wrapper = await createComponent();
await store.dispatch('initializeCycleAnalytics', {
...initialCycleAnalyticsState,
selectedValueStream: null,
});
});
it('sets the created_after and created_before url parameters', async () => {
await shouldMergeUrlParams(wrapper, defaultParams);
});
});
describe('with selectedValueStream set', () => {
beforeEach(async () => {
wrapper = await createComponent();
await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
await wrapper.vm.$nextTick();
});
it('sets the value_stream_id url parameter', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
});
});
}); });
describe('with selectedProjectIds set', () => { describe('with selectedProjectIds set', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = await createComponent();
store.dispatch('setSelectedProjects', mockData.selectedProjects); store.dispatch('setSelectedProjects', mockData.selectedProjects);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
......
...@@ -1047,7 +1047,7 @@ describe('Cycle analytics actions', () => { ...@@ -1047,7 +1047,7 @@ describe('Cycle analytics actions', () => {
}); });
describe('receiveValueStreamsSuccess', () => { describe('receiveValueStreamsSuccess', () => {
it(`commits the ${types.RECEIVE_VALUE_STREAMS_SUCCESS} mutation`, () => { it(`with a selectedValueStream in state commits the ${types.RECEIVE_VALUE_STREAMS_SUCCESS} mutation and dispatches 'fetchValueStreamData'`, () => {
return testAction( return testAction(
actions.receiveValueStreamsSuccess, actions.receiveValueStreamsSuccess,
valueStreams, valueStreams,
...@@ -1058,7 +1058,25 @@ describe('Cycle analytics actions', () => { ...@@ -1058,7 +1058,25 @@ describe('Cycle analytics actions', () => {
payload: valueStreams, payload: valueStreams,
}, },
], ],
[{ type: 'setSelectedValueStream', payload: selectedValueStream.id }], [{ type: 'fetchValueStreamData' }],
);
});
it(`commits the ${types.RECEIVE_VALUE_STREAMS_SUCCESS} mutation and dispatches 'setSelectedValueStream'`, () => {
return testAction(
actions.receiveValueStreamsSuccess,
valueStreams,
{
...state,
selectedValueStream: null,
},
[
{
type: types.RECEIVE_VALUE_STREAMS_SUCCESS,
payload: valueStreams,
},
],
[{ type: 'setSelectedValueStream', payload: selectedValueStream }],
); );
}); });
}); });
......
...@@ -96,9 +96,9 @@ describe('Cycle analytics mutations', () => { ...@@ -96,9 +96,9 @@ describe('Cycle analytics mutations', () => {
describe('with value streams available', () => { describe('with value streams available', () => {
it.each` it.each`
mutation | payload | expectedState mutation | payload | expectedState
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: valueStreams[1] }} ${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
${types.SET_SELECTED_VALUE_STREAM} | ${'fake-id'} | ${{ selectedValueStream: {} }} ${types.SET_SELECTED_VALUE_STREAM} | ${'fake-id'} | ${{ selectedValueStream: {} }}
`( `(
'$mutation with payload $payload will update state with $expectedState', '$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => { ({ mutation, payload, expectedState }) => {
......
...@@ -5,6 +5,12 @@ import { ...@@ -5,6 +5,12 @@ import {
filterBySearchTerm, filterBySearchTerm,
} from 'ee/analytics/shared/utils'; } from 'ee/analytics/shared/utils';
const rawValueStream = `{
"id": 1,
"name": "Custom value stream 1",
"is_custom": true
}`;
const groupDataset = { const groupDataset = {
groupId: '1', groupId: '1',
groupName: 'My Group', groupName: 'My Group',
...@@ -27,13 +33,13 @@ const projectDataset = { ...@@ -27,13 +33,13 @@ const projectDataset = {
projectPathWithNamespace: 'my-group/my-project', projectPathWithNamespace: 'my-group/my-project',
}; };
const rawProjects = JSON.stringify([ const rawProjects = `[
{ {
project_id: '1', "project_id": "1",
project_name: 'My Project', "project_name": "My Project",
project_path_with_namespace: 'my-group/my-project', "project_path_with_namespace": "my-group/my-project"
}, }
]); ]`;
describe('buildGroupFromDataset', () => { describe('buildGroupFromDataset', () => {
it('returns null if groupId is missing', () => { it('returns null if groupId is missing', () => {
...@@ -90,6 +96,28 @@ describe('buildCycleAnalyticsInitialData', () => { ...@@ -90,6 +96,28 @@ describe('buildCycleAnalyticsInitialData', () => {
}); });
}); });
describe('value stream', () => {
it('will be set given an array of projects', () => {
expect(buildCycleAnalyticsInitialData({ valueStream: rawValueStream })).toMatchObject({
selectedValueStream: {
id: 1,
name: 'Custom value stream 1',
isCustom: true,
},
});
});
it.each`
value
${null}
${''}
`('will be null if given a value of `$value`', ({ value }) => {
expect(buildCycleAnalyticsInitialData({ valueStream: value })).toMatchObject({
selectedValueStream: null,
});
});
});
describe('group', () => { describe('group', () => {
it("will be set given a valid 'groupId' and all group parameters", () => { it("will be set given a valid 'groupId' and all group parameters", () => {
expect(buildCycleAnalyticsInitialData(groupDataset)).toMatchObject({ expect(buildCycleAnalyticsInitialData(groupDataset)).toMatchObject({
......
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