Commit c4c1f3e1 authored by Martin Wortschack's avatar Martin Wortschack Committed by Ezekiel Kigbo

Add URL param for linking to a specific stage in group-level VSA

parent 4b5b3064
......@@ -129,6 +129,9 @@ export default {
project_ids: selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
// the `overview` stage is always the default, so dont persist the id if its selected
stage_id:
this.selectedStage?.id && !this.isOverviewStageSelected ? this.selectedStage.id : null,
};
},
stageCount() {
......
......@@ -157,13 +157,13 @@ export const receiveGroupStagesError = ({ commit }, error) => {
});
};
export const setDefaultSelectedStage = ({ dispatch, getters, state: { featureFlags } = {} }) => {
export const setDefaultSelectedStage = ({ state: { featureFlags }, dispatch, getters }) => {
const { activeStages = [] } = getters;
if (featureFlags?.hasPathNavigation) {
return dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
}
const { activeStages = [] } = getters;
if (activeStages?.length) {
const [firstActiveStage] = activeStages;
return Promise.all([
......@@ -175,13 +175,21 @@ export const setDefaultSelectedStage = ({ dispatch, getters, state: { featureFla
createFlash({
message: __('There was an error while fetching value stream analytics data.'),
});
return Promise.resolve();
};
export const receiveGroupStagesSuccess = ({ commit, dispatch }, stages) => {
export const receiveGroupStagesSuccess = (
{ state: { featureFlags }, commit, dispatch },
stages,
) => {
commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages);
if (!featureFlags?.hasPathNavigation) {
return dispatch('setDefaultSelectedStage');
}
return Promise.resolve();
};
export const fetchGroupStagesAndEvents = ({ dispatch, getters }) => {
......@@ -309,12 +317,17 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
stage: selectedStage,
group,
} = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags);
if (group?.fullPath) {
return Promise.all([
selectedStage
? dispatch('setSelectedStage', selectedStage)
: dispatch('setDefaultSelectedStage'),
selectedStage?.id ? dispatch('fetchStageData', selectedStage.id) : Promise.resolve(),
dispatch('setPaths', { groupPath: group.fullPath, milestonesPath, labelsPath }),
dispatch('filters/initialize', {
selectedAuthor,
......
......@@ -438,7 +438,7 @@ export const transformStagesForPathNavigation = ({ stages, medians, selectedStag
const formattedStages = stages.map((stage) => {
return {
metric: medians[stage?.id],
selected: stage.title === selectedStage.title,
selected: stage.id === selectedStage.id,
icon: null,
...stage,
};
......
......@@ -105,6 +105,7 @@ export const buildCycleAnalyticsInitialData = ({
labelsPath = '',
milestonesPath = '',
defaultStages = null,
stage = null,
} = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId
......@@ -128,6 +129,7 @@ export const buildCycleAnalyticsInitialData = ({
defaultStageConfig: defaultStages
? buildDefaultStagesFromJSON(defaultStages).map(convertObjectPropsToCamelCase)
: [],
stage: JSON.parse(stage),
});
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
......
......@@ -7,6 +7,7 @@ module Gitlab
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Attributes
include Gitlab::Utils::StrongMemoize
MAX_RANGE_DAYS = 180.days.freeze
DEFAULT_DATE_RANGE = 29.days # 30 including Date.today
......@@ -19,6 +20,7 @@ module Gitlab
:sort,
:direction,
:page,
:stage_id,
label_name: [].freeze,
assignee_username: [].freeze,
project_ids: [].freeze
......@@ -41,6 +43,7 @@ module Gitlab
attribute :sort
attribute :direction
attribute :page
attribute :stage_id
FINDER_PARAM_NAMES.each do |param_name|
attribute param_name
......@@ -88,6 +91,7 @@ module Gitlab
attrs[:milestone] = milestone_title if milestone_title.present?
attrs[:sort] = sort if sort.present?
attrs[:direction] = direction if direction.present?
attrs[:stage] = stage_data_attributes.to_json if stage_id.present?
end
end
......@@ -133,6 +137,15 @@ module Gitlab
}
end
def stage_data_attributes
return unless stage
{
id: stage.id || stage.name,
title: stage.name
}
end
def validate_created_before
return if created_after.nil? || created_before.nil?
......@@ -154,6 +167,14 @@ module Gitlab
DEFAULT_DATE_RANGE.ago
end
end
def stage
return unless value_stream
strong_memoize(:stage) do
::Analytics::CycleAnalytics::StageFinder.new(parent: group, stage_id: stage_id).execute if stage_id
end
end
end
end
end
......
......@@ -33,6 +33,7 @@ const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
const currentGroup = convertObjectPropsToCamelCase(mockData.group);
const emptyStateSvgPath = 'path/to/empty/state';
const stage = null;
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -60,6 +61,7 @@ const initialCycleAnalyticsState = {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
group: currentGroup,
stage,
};
const mocks = {
......@@ -610,6 +612,7 @@ describe('Value Stream Analytics component', () => {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
stage_id: null,
};
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => getIdFromGraphQLId(id));
......@@ -663,7 +666,7 @@ describe('Value Stream Analytics component', () => {
describe('with selectedProjectIds set', () => {
beforeEach(async () => {
wrapper = await createComponent();
store.dispatch('setSelectedProjects', mockData.selectedProjects);
await store.dispatch('setSelectedProjects', mockData.selectedProjects);
await wrapper.vm.$nextTick();
});
......@@ -673,6 +676,30 @@ describe('Value Stream Analytics component', () => {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: selectedProjectIds,
stage_id: 1,
});
});
});
describe('with selectedStage set', () => {
const selectedStage = {
title: 'Plan',
id: 2,
};
beforeEach(async () => {
wrapper = await createComponent();
store.dispatch('setSelectedStage', selectedStage);
await wrapper.vm.$nextTick();
});
it('sets the stage_id url parameter', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
stage_id: 2,
});
});
});
......
......@@ -373,7 +373,45 @@ describe('Value Stream Analytics actions', () => {
});
describe('receiveGroupStagesSuccess', () => {
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation and dispatches 'setDefaultSelectedStage'`, () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: true,
},
};
});
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[],
);
});
});
describe('when the `hasPathNavigation` feature flag is disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: false,
},
};
});
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation and dispatches 'setDefaultSelectedStage`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
......@@ -388,6 +426,7 @@ describe('Value Stream Analytics actions', () => {
);
});
});
});
describe('setDefaultSelectedStage', () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => {
......@@ -450,7 +489,7 @@ describe('Value Stream Analytics actions', () => {
${null}
`('with $data will flash an error', ({ data }) => {
actions.setDefaultSelectedStage(
{ getters: { activeStages: data }, dispatch: () => {} },
{ state, getters: { activeStages: data }, dispatch: () => {} },
{},
);
expect(createFlash).toHaveBeenCalledWith({ message: flashErrorMessage });
......@@ -876,6 +915,30 @@ describe('Value Stream Analytics actions', () => {
expect(mockDispatch).toHaveBeenCalledWith('initializeCycleAnalyticsSuccess');
});
describe('with a selected stage', () => {
it('dispatches "setSelectedStage" and "fetchStageData"', async () => {
const stage = { id: 2, title: 'plan' };
await actions.initializeCycleAnalytics(store, {
...initialData,
stage,
});
expect(mockDispatch).toHaveBeenCalledWith('setSelectedStage', stage);
expect(mockDispatch).toHaveBeenCalledWith('fetchStageData', stage.id);
});
});
describe('without a selected stage', () => {
it('dispatches "setDefaultSelectedStage"', async () => {
await actions.initializeCycleAnalytics(store, {
...initialData,
stage: null,
});
expect(mockDispatch).not.toHaveBeenCalledWith('setSelectedStage');
expect(mockDispatch).not.toHaveBeenCalledWith('fetchStageData');
expect(mockDispatch).toHaveBeenCalledWith('setDefaultSelectedStage');
});
});
it('commits "INITIALIZE_VSA"', async () => {
await actions.initializeCycleAnalytics(store, initialData);
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', initialData);
......
......@@ -90,6 +90,7 @@ describe('buildCycleAnalyticsInitialData', () => {
${'selectedProjects'} | ${[]}
${'labelsPath'} | ${''}
${'milestonesPath'} | ${''}
${'stage'} | ${null}
`('will set a default value for "$field" if is not present', ({ field, value }) => {
expect(buildCycleAnalyticsInitialData()).toMatchObject({
[field]: value,
......
......@@ -189,12 +189,16 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do
end
describe 'issuable filter params' do
let_it_be(:stage) { create(:cycle_analytics_group_stage, group: root_group) }
before do
params.merge!(
milestone_title: 'title',
assignee_username: ['username1'],
label_name: %w[label1 label2],
author_username: 'author'
author_username: 'author',
stage_id: stage.id,
value_stream: stage.value_stream
)
end
......@@ -204,6 +208,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it { expect(subject[:assignees]).to eq('["username1"]') }
it { expect(subject[:labels]).to eq('["label1","label2"]') }
it { expect(subject[:author]).to eq('author') }
it { expect(subject[:stage]).to eq('{"id":1,"title":"Stage #1"}') }
end
describe 'sorting params' do
......
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