Commit 9d45cf50 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'feat-add-recover-hidden-stages-dropdown' into 'master'

Recover hidden cycle analytics stages

See merge request gitlab-org/gitlab!25309
parents 8f91c1e6 6fe5b635
......@@ -192,7 +192,7 @@
.stage-events {
width: 60%;
overflow: scroll;
height: 467px;
min-height: 467px;
}
.stage-event-list {
......
......@@ -76,6 +76,7 @@ export default {
'durationChartPlottableData',
'tasksByTypeChartData',
'durationChartMedianData',
'activeStages',
]),
shouldRenderEmptyState() {
return !this.selectedGroup;
......@@ -271,7 +272,7 @@ export default {
v-if="selectedStage"
class="js-stage-table"
:current-stage="selectedStage"
:stages="stages"
:stages="activeStages"
:medians="medians"
:is-loading="isLoadingStage"
:is-empty-stage="isEmptyStage"
......
<script>
import { mapGetters } from 'vuex';
import { isEqual } from 'underscore';
import { GlFormGroup, GlFormInput, GlFormSelect, GlLoadingIcon } from '@gitlab/ui';
import {
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLoadingIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownItem,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import LabelsSelector from './labels_selector.vue';
......@@ -30,6 +39,9 @@ export default {
GlFormSelect,
GlLoadingIcon,
LabelsSelector,
GlDropdown,
GlDropdownHeader,
GlDropdownItem,
},
props: {
events: {
......@@ -79,6 +91,7 @@ export default {
};
},
computed: {
...mapGetters(['hiddenStages']),
startEventOptions() {
return [
{ value: null, text: s__('CustomCycleAnalytics|Select start event') },
......@@ -148,6 +161,9 @@ export default {
? s__('CustomCycleAnalytics|Editing stage')
: s__('CustomCycleAnalytics|New stage');
},
hasHiddenStages() {
return this.hiddenStages.length;
},
},
watch: {
initialFields(newFields) {
......@@ -202,13 +218,28 @@ export default {
onUpdateEndEventField() {
this.$set(this.fieldErrors, 'endEventIdentifier', null);
},
handleRecoverStage(id) {
this.$emit(STAGE_ACTIONS.UPDATE, { id, hidden: false });
},
},
};
</script>
<template>
<form class="custom-stage-form m-4 mt-0">
<div class="mb-1">
<div class="mb-1 d-flex flex-row justify-content-between">
<h4>{{ formTitle }}</h4>
<gl-dropdown :text="__('Recover hidden stage')" class="js-recover-hidden-stage-dropdown">
<gl-dropdown-header>{{ __('Default stages') }}</gl-dropdown-header>
<template v-if="hasHiddenStages">
<gl-dropdown-item
v-for="stage in hiddenStages"
:key="stage.id"
@click="handleRecoverStage(stage.id)"
>{{ stage.title }}</gl-dropdown-item
>
</template>
<p v-else class="mx-3 my-2">{{ __('All default stages are currently visible') }}</p>
</gl-dropdown>
</div>
<gl-form-group
......
......@@ -48,3 +48,9 @@ export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
}
return { groupBy: [], data: [], seriesNames: [] };
};
const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
export const hiddenStages = ({ stages }) => filterStagesByHiddenStatus(stages);
export const activeStages = ({ stages }) => filterStagesByHiddenStatus(stages, false);
......@@ -137,7 +137,7 @@ export default {
},
[types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS](state, data) {
const { events = [], stages = [] } = data;
state.stages = transformRawStages(stages.filter(({ hidden = false }) => !hidden));
state.stages = transformRawStages(stages);
state.customStageFormEvents = events.map(ev =>
convertObjectPropsToCamelCase(ev, { deep: true }),
......
......@@ -299,7 +299,7 @@ describe 'Group Value Stream Analytics', :js do
start_label_event = :issue_label_added
stop_label_event = :issue_label_removed
let(:button_class) { '.js-add-stage-button' }
let(:add_stage_button) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
let(:first_default_stage) { page.find('.stage-nav-item-cell', text: "Issue").ancestor(".stage-nav-item") }
let(:first_custom_stage) { page.find('.stage-nav-item-cell', text: custom_stage_name).ancestor(".stage-nav-item") }
......@@ -334,34 +334,34 @@ describe 'Group Value Stream Analytics', :js do
context 'Add a stage button' do
it 'is visible' do
expect(page).to have_selector(button_class, visible: true)
expect(page).to have_selector(add_stage_button, visible: true)
expect(page).to have_text('Add a stage')
end
it 'becomes active when clicked' do
expect(page).not_to have_selector("#{button_class}.active")
expect(page).not_to have_selector("#{add_stage_button}.active")
find(button_class).click
find(add_stage_button).click
expect(page).to have_selector("#{button_class}.active")
expect(page).to have_selector("#{add_stage_button}.active")
end
it 'displays the custom stage form when clicked' do
expect(page).not_to have_text('New stage')
page.find(button_class).click
page.find(add_stage_button).click
expect(page).to have_text('New stage')
end
end
context 'Custom stage form' do
let(:show_form_button_class) { '.js-add-stage-button' }
let(:show_form_add_stage_button) { '.js-add-stage-button' }
before do
select_group
page.find(show_form_button_class).click
page.find(show_form_add_stage_button).click
wait_for_requests
end
......@@ -535,6 +535,25 @@ describe 'Group Value Stream Analytics', :js do
context 'Stage table' do
context 'default stages' do
let(:nav) { page.find(stage_nav_selector) }
def open_recover_stage_dropdown
find(add_stage_button).click
expect(page).to have_content('New stage')
expect(page).to have_content('Recover hidden stage')
click_button "Recover hidden stage"
within(:css, '.js-recover-hidden-stage-dropdown') do
expect(find(".dropdown-menu")).to have_content('Default stages')
end
end
def active_stages
page.all(".stage-nav .stage-name").collect(&:text)
end
before do
select_group
......@@ -553,14 +572,43 @@ describe 'Group Value Stream Analytics', :js do
expect(first_default_stage.find('.more-actions-dropdown')).not_to have_text "Remove stage"
end
it 'will not appear in the stage table after being hidden' do
nav = page.find(stage_nav_selector)
expect(nav).to have_text("Issue")
context 'hidden' do
before do
click_button "Hide stage"
click_button "Hide stage"
# wait for the stage list to laod
expect(nav).to have_content("Plan")
end
expect(page.find('.flash-notice')).to have_text 'Stage data updated'
expect(nav).not_to have_text("Issue")
it 'will not appear in the stage table' do
expect(active_stages).not_to include("Issue")
end
it 'can be recovered' do
open_recover_stage_dropdown
expect(page.find('.js-recover-hidden-stage-dropdown')).to have_text('Issue')
end
end
context 'recovered' do
before do
click_button "Hide stage"
# wait for the stage list to laod
expect(nav).to have_content("Plan")
end
it 'will appear in the stage table' do
open_recover_stage_dropdown
click_button("Issue")
# wait for the stage list to laod
expect(nav).to have_content("Plan")
expect(page.find('.flash-notice')).to have_content 'Stage data updated'
expect(active_stages).to include("Issue")
end
end
end
......
......@@ -7,7 +7,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
`;
exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__123\\">
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__177\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
......@@ -30,7 +30,7 @@ exports[`CustomStageForm Start event with events does not select events with can
`;
exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__95\\">
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__137\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
......
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import Vuex from 'vuex';
import createStore from 'ee/analytics/cycle_analytics/store';
import { createLocalVue, mount } from '@vue/test-utils';
import CustomStageForm from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants';
import {
......@@ -21,14 +23,22 @@ const initData = {
endEventLabelId: groupLabels[1].id,
};
let store = null;
const localVue = createLocalVue();
localVue.use(Vuex);
describe('CustomStageForm', () => {
function createComponent(props) {
function createComponent(props = {}, stubs = {}) {
store = createStore();
return mount(CustomStageForm, {
localVue,
store,
propsData: {
events,
labels: groupLabels,
...props,
},
stubs,
});
}
......@@ -44,6 +54,9 @@ describe('CustomStageForm', () => {
submit: '.js-save-stage',
cancel: '.js-save-stage-cancel',
invalidFeedback: '.invalid-feedback',
recoverStageDropdown: '.js-recover-hidden-stage-dropdown',
recoverStageDropdownTrigger: '.js-recover-hidden-stage-dropdown .dropdown-toggle',
hiddenStageDropdownOption: '.js-recover-hidden-stage-dropdown .dropdown-item',
};
function getDropdownOption(_wrapper, dropdown, index) {
......@@ -122,7 +135,7 @@ describe('CustomStageForm', () => {
describe('start event label', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
wrapper = createComponent();
});
afterEach(() => {
......@@ -174,7 +187,7 @@ describe('CustomStageForm', () => {
const currAllowed = startEvents[startEventArrayIndex].allowedEndEvents;
beforeEach(() => {
wrapper = createComponent({}, false);
wrapper = createComponent();
});
it('notifies that a start event needs to be selected first', () => {
......@@ -251,7 +264,7 @@ describe('CustomStageForm', () => {
describe('with a stop event selected and a change to the start event', () => {
beforeEach(() => {
wrapper = createComponent({});
wrapper = createComponent();
wrapper.setData({
fields: {
......@@ -518,15 +531,12 @@ describe('CustomStageForm', () => {
describe('Editing a custom stage', () => {
beforeEach(() => {
wrapper = createComponent(
{
isEditingCustomStage: true,
initialFields: {
...initData,
},
wrapper = createComponent({
isEditingCustomStage: true,
initialFields: {
...initData,
},
false,
);
});
wrapper.setData({
fields: {
......@@ -682,4 +692,61 @@ describe('CustomStageForm', () => {
expect(wrapper.find({ ref: 'startEventIdentifier' }).html()).toContain('cant be blank');
});
});
describe('recover stage dropdown', () => {
const formFieldStubs = {
'gl-form-group': true,
'gl-form-select': true,
'labels-selector': true,
};
beforeEach(() => {
wrapper = createComponent({}, formFieldStubs);
});
describe('without hidden stages', () => {
it('has the recover stage dropdown', () => {
expect(wrapper.find(sel.recoverStageDropdown).exists()).toBe(true);
});
it('has no stages available to recover', () => {
wrapper.find(sel.recoverStageDropdownTrigger).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(sel.recoverStageDropdown).text()).toContain(
'All default stages are currently visible',
);
});
});
});
describe('with hidden stages', () => {
beforeEach(() => {
wrapper = createComponent({}, formFieldStubs);
store.state.stages = [{ id: 'my-stage', title: 'My default stage', hidden: true }];
});
it('has stages available to recover', () => {
wrapper.find(sel.recoverStageDropdownTrigger).trigger('click');
return wrapper.vm.$nextTick().then(() => {
const txt = wrapper.find(sel.recoverStageDropdown).text();
expect(txt).not.toContain('All default stages are currently visible');
expect(txt).toContain('My default stage');
});
});
it(`emits the ${STAGE_ACTIONS.UPDATE} action when clicking on a stage to recover`, () => {
wrapper.find(sel.recoverStageDropdownTrigger).trigger('click');
return wrapper.vm.$nextTick().then(() => {
wrapper
.findAll(sel.hiddenStageDropdownOption)
.at(0)
.trigger('click');
expect(wrapper.emitted()).toEqual({
[STAGE_ACTIONS.UPDATE]: [[{ hidden: false, id: 'my-stage' }]],
});
});
});
});
});
});
......@@ -6,6 +6,7 @@ import {
transformedDurationMedianData,
durationChartPlottableData,
durationChartPlottableMedianData,
allowedStages,
} from '../mock_data';
let state = null;
......@@ -121,4 +122,20 @@ describe('Cycle analytics getters', () => {
expect(getters.durationChartMedianData(stateWithDurationMedianData)).toEqual([]);
});
});
const hiddenStage = { ...allowedStages[2], hidden: true };
const givenStages = [allowedStages[0], allowedStages[1], hiddenStage];
describe.each`
func | givenStages | expectedStages
${'hiddenStages'} | ${givenStages} | ${[hiddenStage]}
${'activeStages'} | ${givenStages} | ${[allowedStages[0], allowedStages[1]]}
`('hiddenStages', ({ func, expectedStages, givenStages: stages }) => {
it(`'${func}' returns ${expectedStages.length} stages`, () => {
expect(getters[func]({ stages })).toEqual(expectedStages);
});
it(`'${func}' returns an empty array if there are no stages`, () => {
expect(getters[func]({ stages: [] })).toEqual([]);
});
});
});
......@@ -1538,6 +1538,9 @@ msgstr ""
msgid "All changes are committed"
msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits."
msgstr ""
......@@ -6110,6 +6113,9 @@ msgstr ""
msgid "Default projects limit"
msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Directly import the Google Code email address or username"
msgstr ""
......@@ -15727,6 +15733,9 @@ msgstr ""
msgid "Recipe"
msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovery Codes"
msgstr ""
......
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