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 {
'medians',
'isLoadingValueStreams',
'selectedStageError',
'selectedValueStream',
]),
// 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
......@@ -106,6 +107,7 @@ export default {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
return {
value_stream_id: this.selectedValueStream?.id || null,
project_ids: selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
......
......@@ -129,8 +129,8 @@ export default {
isSelected(id) {
return Boolean(this.selectedValueStreamId && this.selectedValueStreamId === id);
},
onSelect(id) {
this.setSelectedValueStream(id);
onSelect(selectedId) {
this.setSelectedValueStream(this.data.find(({ id }) => id === selectedId));
},
onDelete() {
const name = this.selectedValueStreamName;
......
......@@ -61,3 +61,5 @@ export const OVERVIEW_METRICS = {
TIME_SUMMARY: 'TIME_SUMMARY',
RECENT_ACTIVITY: 'RECENT_ACTIVITY',
};
export const FETCH_VALUE_STREAM_DATA = 'fetchValueStreamData';
......@@ -3,6 +3,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { FETCH_VALUE_STREAM_DATA } from '../constants';
import {
removeFlash,
throwIfUserForbidden,
......@@ -373,16 +374,19 @@ export const fetchValueStreamData = ({ dispatch }) =>
export const setSelectedValueStream = ({ commit, dispatch }, 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);
if (data.length) {
if (!selectedValueStream && data.length) {
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 }) => {
......@@ -404,7 +408,7 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
throw error;
});
}
return dispatch('fetchValueStreamData');
return dispatch(FETCH_VALUE_STREAM_DATA);
};
export const setFilters = ({ dispatch }) => {
......
......@@ -91,11 +91,13 @@ export default {
createdAfter: startDate = null,
createdBefore: endDate = null,
selectedProjects = [],
selectedValueStream = {},
} = {},
) {
state.isLoading = true;
state.currentGroup = group;
state.selectedProjects = selectedProjects;
state.selectedValueStream = selectedValueStream;
state.startDate = startDate;
state.endDate = endDate;
},
......@@ -138,8 +140,8 @@ export default {
state.isDeletingValueStream = false;
state.deleteValueStreamError = null;
},
[types.SET_SELECTED_VALUE_STREAM](state, streamId) {
state.selectedValueStream = state.valueStreams?.find(({ id }) => id === streamId) || null;
[types.SET_SELECTED_VALUE_STREAM](state, valueStream) {
state.selectedValueStream = valueStream;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.isLoadingValueStreams = true;
......
......@@ -10,6 +10,17 @@ export default {
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.
*
......@@ -71,6 +82,7 @@ const buildProjectsFromJSON = (projects = []) => {
* @returns {Object} - The initial data to load the app with
*/
export const buildCycleAnalyticsInitialData = ({
valueStream = null,
groupId = null,
createdBefore = null,
createdAfter = null,
......@@ -82,6 +94,7 @@ export const buildCycleAnalyticsInitialData = ({
labelsPath = '',
milestonesPath = '',
} = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId
? convertObjectPropsToCamelCase(
buildGroupFromDataset({
......
......@@ -69,7 +69,7 @@ module Gitlab
def to_data_attributes
{}.tap do |attrs|
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_before] = created_before.to_date.iso8601
attrs[:projects] = group_projects(project_ids) if group && project_ids.present?
......@@ -94,7 +94,9 @@ module Gitlab
def value_stream_data_attributes
{
id: value_stream.id
id: value_stream.id,
name: value_stream.name,
is_custom: value_stream.custom?
}
end
......
......@@ -52,7 +52,10 @@ const defaultFeatureFlags = {
hasCreateMultipleValueStreams: false,
};
const [selectedValueStream] = mockData.valueStreams;
const initialCycleAnalyticsState = {
selectedValueStream,
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
group: currentGroup,
......@@ -627,6 +630,7 @@ describe('Cycle Analytics component', () => {
describe('Url parameters', () => {
const defaultParams = {
value_stream_id: selectedValueStream.id,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
......@@ -640,9 +644,6 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent();
await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
});
afterEach(() => {
......@@ -651,12 +652,41 @@ describe('Cycle Analytics component', () => {
wrapper = null;
});
it('sets the created_after and created_before url parameters', async () => {
await shouldMergeUrlParams(wrapper, defaultParams);
describe('with minimal parameters set set', () => {
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', () => {
beforeEach(async () => {
wrapper = await createComponent();
store.dispatch('setSelectedProjects', mockData.selectedProjects);
await wrapper.vm.$nextTick();
});
......
......@@ -1047,7 +1047,7 @@ describe('Cycle analytics actions', () => {
});
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(
actions.receiveValueStreamsSuccess,
valueStreams,
......@@ -1058,7 +1058,25 @@ describe('Cycle analytics actions', () => {
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', () => {
describe('with value streams available', () => {
it.each`
mutation | payload | expectedState
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: valueStreams[1] }}
${types.SET_SELECTED_VALUE_STREAM} | ${'fake-id'} | ${{ selectedValueStream: {} }}
mutation | payload | expectedState
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
${types.SET_SELECTED_VALUE_STREAM} | ${'fake-id'} | ${{ selectedValueStream: {} }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......
......@@ -5,6 +5,12 @@ import {
filterBySearchTerm,
} from 'ee/analytics/shared/utils';
const rawValueStream = `{
"id": 1,
"name": "Custom value stream 1",
"is_custom": true
}`;
const groupDataset = {
groupId: '1',
groupName: 'My Group',
......@@ -27,13 +33,13 @@ const projectDataset = {
projectPathWithNamespace: 'my-group/my-project',
};
const rawProjects = JSON.stringify([
const rawProjects = `[
{
project_id: '1',
project_name: 'My Project',
project_path_with_namespace: 'my-group/my-project',
},
]);
"project_id": "1",
"project_name": "My Project",
"project_path_with_namespace": "my-group/my-project"
}
]`;
describe('buildGroupFromDataset', () => {
it('returns null if groupId is missing', () => {
......@@ -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', () => {
it("will be set given a valid 'groupId' and all group parameters", () => {
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