Commit 26d4d5b3 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '267536-extract-custom-stage-fields' into 'master'

Extract custom value stream form fields

See merge request gitlab-org/gitlab!50020
parents bd0dab96 166193b5
import { __, s__ } from '~/locale';
export const I18N = {
RECOVER_HIDDEN_STAGE: s__('CustomCycleAnalytics|Recover hidden stage'),
RECOVER_STAGE_TITLE: s__('CustomCycleAnalytics|Default stages'),
RECOVER_STAGES_VISIBLE: s__('CustomCycleAnalytics|All default stages are currently visible'),
SELECT_START_EVENT: s__('CustomCycleAnalytics|Select start event'),
SELECT_END_EVENT: s__('CustomCycleAnalytics|Select end event'),
FORM_FIELD_NAME: s__('CustomCycleAnalytics|Name'),
FORM_FIELD_NAME_PLACEHOLDER: s__('CustomCycleAnalytics|Enter a name for the stage'),
FORM_FIELD_START_EVENT: s__('CustomCycleAnalytics|Start event'),
FORM_FIELD_START_EVENT_LABEL: s__('CustomCycleAnalytics|Start event label'),
FORM_FIELD_END_EVENT: s__('CustomCycleAnalytics|End event'),
FORM_FIELD_END_EVENT_LABEL: s__('CustomCycleAnalytics|End event label'),
BTN_UPDATE_STAGE: s__('CustomCycleAnalytics|Update stage'),
BTN_ADD_STAGE: s__('CustomCycleAnalytics|Add stage'),
TITLE_EDIT_STAGE: s__('CustomCycleAnalytics|Editing stage'),
TITLE_ADD_STAGE: s__('CustomCycleAnalytics|New stage'),
BTN_CANCEL: __('Cancel'),
};
export const ERRORS = {
START_EVENT_REQUIRED: s__('CustomCycleAnalytics|Please select a start event first'),
STAGE_NAME_EXISTS: s__('CustomCycleAnalytics|Stage name already exists'),
INVALID_EVENT_PAIRS: s__(
'CustomCycleAnalytics|Start event changed, please select a valid end event',
),
};
export const defaultErrors = {
id: [],
name: [],
startEventIdentifier: [],
startEventLabelId: [],
endEventIdentifier: [],
endEventLabelId: [],
};
export const defaultFields = {
id: null,
name: null,
startEventIdentifier: null,
startEventLabelId: null,
endEventIdentifier: null,
endEventLabelId: null,
};
<script>
import { GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import LabelsSelector from '../labels_selector.vue';
import { I18N } from './constants';
import { startEventOptions, endEventOptions } from './utils';
import { isLabelEvent } from '../../utils';
export default {
name: 'CustomStageFormFields',
components: {
GlFormGroup,
GlFormInput,
GlFormSelect,
LabelsSelector,
},
props: {
fields: {
type: Object,
required: true,
},
errors: {
type: Object,
required: false,
default: () => {},
},
events: {
type: Array,
required: true,
},
labelEvents: {
type: Array,
required: true,
},
},
computed: {
startEvents() {
return startEventOptions(this.events);
},
endEvents() {
return endEventOptions(this.events, this.fields.startEventIdentifier);
},
hasStartEvent() {
return this.fields.startEventIdentifier;
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
},
endEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
},
},
methods: {
eventFieldClasses(condition) {
return condition ? 'gl-w-half gl-mr-2' : 'gl-w-full';
},
hasFieldErrors(key) {
return this.errors[key]?.length < 1;
},
fieldErrorMessage(key) {
return this.errors[key]?.join('\n');
},
handleUpdateField(field, value) {
this.$emit('update', field, value);
},
},
I18N,
};
</script>
<template>
<div>
<gl-form-group
:label="$options.I18N.FORM_FIELD_NAME"
label-for="custom-stage-name"
:state="hasFieldErrors('name')"
:invalid-feedback="fieldErrorMessage('name')"
data-testid="custom-stage-name"
>
<gl-form-input
:value="fields.name"
class="form-control"
type="text"
name="custom-stage-name"
:placeholder="$options.I18N.FORM_FIELD_NAME_PLACEHOLDER"
required
@input="handleUpdateField('name', $event)"
/>
</gl-form-group>
<div class="d-flex" :class="{ 'gl-justify-content-between': startEventRequiresLabel }">
<div :class="eventFieldClasses(startEventRequiresLabel)">
<gl-form-group
data-testid="custom-stage-start-event"
:label="$options.I18N.FORM_FIELD_START_EVENT"
label-for="custom-stage-start-event"
:state="hasFieldErrors('startEventIdentifier')"
:invalid-feedback="fieldErrorMessage('startEventIdentifier')"
>
<gl-form-select
v-model="fields.startEventIdentifier"
name="custom-stage-start-event"
:required="true"
:options="startEvents"
@input="handleUpdateField('startEventIdentifier', $event)"
/>
</gl-form-group>
</div>
<div v-if="startEventRequiresLabel" class="gl-w-half gl-ml-2">
<gl-form-group
data-testid="custom-stage-start-event-label"
:label="$options.I18N.FORM_FIELD_START_EVENT_LABEL"
label-for="custom-stage-start-event-label"
:state="hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:selected-label-id="[fields.startEventLabelId]"
name="custom-stage-start-event-label"
@selectLabel="handleUpdateField('startEventLabelId', $event)"
/>
</gl-form-group>
</div>
</div>
<div class="d-flex" :class="{ 'gl-justify-content-between': endEventRequiresLabel }">
<div :class="eventFieldClasses(endEventRequiresLabel)">
<gl-form-group
data-testid="custom-stage-end-event"
:label="$options.I18N.FORM_FIELD_END_EVENT"
label-for="custom-stage-end-event"
:state="hasFieldErrors('endEventIdentifier')"
:invalid-feedback="fieldErrorMessage('endEventIdentifier')"
>
<gl-form-select
v-model="fields.endEventIdentifier"
name="custom-stage-end-event"
:options="endEvents"
:required="true"
:disabled="!hasStartEvent"
@input="handleUpdateField('endEventIdentifier', $event)"
/>
</gl-form-group>
</div>
<div v-if="endEventRequiresLabel" class="w-50 ml-1">
<gl-form-group
data-testid="custom-stage-end-event-label"
:label="$options.I18N.FORM_FIELD_END_EVENT_LABEL"
label-for="custom-stage-end-event-label"
:state="hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:selected-label-id="[fields.endEventLabelId]"
name="custom-stage-end-event-label"
@selectLabel="handleUpdateField('endEventLabelId', $event)"
/>
</gl-form-group>
</div>
</div>
</div>
</template>
import { isStartEvent, getAllowedEndEvents, eventToOption, eventsByIdentifier } from '../../utils';
import { I18N, ERRORS, defaultErrors, defaultFields } from './constants';
import { DEFAULT_STAGE_NAMES } from '../../constants';
/**
* @typedef {Object} CustomStageEvents
* @property {String} canBeStartEvent - Title of the metric measured
* @property {String} name - Friendly name for the event
* @property {String} identifier - snakeized name for the event
*
* @typedef {Object} DropdownData
* @property {String} text - Friendly name for the event
* @property {String} value - Value to be submitted for the dropdown
*/
/**
* Takes an array of custom stage events to return only the
* events where `canBeStartEvent` is true and converts them
* to { value, text } pairs for use in dropdowns
*
* @param {CustomStageEvents[]} events
* @returns {DropdownData[]} array of start events formatted for dropdowns
*/
export const startEventOptions = eventsList => [
{ value: null, text: I18N.SELECT_START_EVENT },
...eventsList.filter(isStartEvent).map(eventToOption),
];
/**
* Takes an array of custom stage events to return only the
* events where `canBeStartEvent` is false and converts them
* to { value, text } pairs for use in dropdowns
*
* @param {CustomStageEvents[]} events
* @returns {DropdownData[]} array end events formatted for dropdowns
*/
export const endEventOptions = (eventsList, startEventIdentifier) => {
const endEvents = getAllowedEndEvents(eventsList, startEventIdentifier);
return [
{ value: null, text: I18N.SELECT_END_EVENT },
...eventsByIdentifier(eventsList, endEvents).map(eventToOption),
];
};
/**
* @typedef {Object} CustomStageFormData
* @property {Object.<String, String>} fields - form field values
* @property {Object.<String, Array>} errors - form field errors
*/
/**
* Initializes the fields and errors for the custom stages form
* providing defaults for any missing keys
*
* @param {CustomStageFormData} data
* @returns {CustomStageFormData} the updated initial data with all defaults
*/
export const initializeFormData = ({ fields, errors }) => {
const initErrors = fields?.endEventIdentifier
? defaultErrors
: {
...defaultErrors,
endEventIdentifier: !fields?.startEventIdentifier ? [ERRORS.START_EVENT_REQUIRED] : [],
};
return {
fields: {
...defaultFields,
...fields,
},
errors: {
...initErrors,
...errors,
},
};
};
/**
* Validates the form fields for the custom stages form
* Any errors will be returned in a object where the key is
* the name of the field.g
*
* @param {Object} fields key value pair of form field values
* @returns {Object} key value pair of form fields with an array of errors
*/
export const validateFields = fields => {
const newErrors = {};
if (fields?.name) {
newErrors.name = DEFAULT_STAGE_NAMES.includes(fields?.name.toLowerCase())
? [ERRORS.STAGE_NAME_EXISTS]
: [];
}
if (fields?.startEventIdentifier) {
newErrors.endEventIdentifier = [];
} else {
newErrors.endEventIdentifier = [ERRORS.START_EVENT_REQUIRED];
}
if (fields?.startEventIdentifier && fields?.endEventIdentifier) {
newErrors.endEventIdentifier = [];
}
return newErrors;
};
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { import {
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLoadingIcon, GlLoadingIcon,
GlDropdown, GlDropdown,
GlDropdownSectionHeader, GlDropdownSectionHeader,
...@@ -12,76 +9,22 @@ import { ...@@ -12,76 +9,22 @@ import {
GlSprintf, GlSprintf,
GlButton, GlButton,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import LabelsSelector from './labels_selector.vue'; import CustomStageFormFields from './create_value_stream_form/custom_stage_fields.vue';
import { STAGE_ACTIONS, DEFAULT_STAGE_NAMES } from '../constants'; import { validateFields, initializeFormData } from './create_value_stream_form/utils';
import { import { defaultFields, ERRORS, I18N } from './create_value_stream_form/constants';
isStartEvent, import { STAGE_ACTIONS } from '../constants';
isLabelEvent, import { getAllowedEndEvents, getLabelEventsIdentifiers, isLabelEvent } from '../utils';
getAllowedEndEvents,
eventToOption,
eventsByIdentifier,
getLabelEventsIdentifiers,
} from '../utils';
const defaultFields = {
id: null,
name: null,
startEventIdentifier: null,
startEventLabelId: null,
endEventIdentifier: null,
endEventLabelId: null,
};
const defaultErrors = {
id: [],
name: [],
startEventIdentifier: [],
startEventLabelId: [],
endEventIdentifier: [],
endEventLabelId: [],
};
const ERRORS = {
START_EVENT_REQUIRED: s__('CustomCycleAnalytics|Please select a start event first'),
STAGE_NAME_EXISTS: s__('CustomCycleAnalytics|Stage name already exists'),
INVALID_EVENT_PAIRS: s__(
'CustomCycleAnalytics|Start event changed, please select a valid stop event',
),
};
export const initializeFormData = ({ emptyFieldState = defaultFields, fields, errors }) => {
const initErrors = fields?.endEventIdentifier
? defaultErrors
: {
...defaultErrors,
endEventIdentifier: !fields?.startEventIdentifier ? [ERRORS.START_EVENT_REQUIRED] : [],
};
return {
fields: {
...emptyFieldState,
...fields,
},
errors: {
...initErrors,
...errors,
},
};
};
export default { export default {
components: { components: {
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLoadingIcon, GlLoadingIcon,
LabelsSelector,
GlDropdown, GlDropdown,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlSprintf, GlSprintf,
GlButton, GlButton,
CustomStageFormFields,
}, },
props: { props: {
events: { events: {
...@@ -93,7 +36,7 @@ export default { ...@@ -93,7 +36,7 @@ export default {
return { return {
labelEvents: getLabelEventsIdentifiers(this.events), labelEvents: getLabelEventsIdentifiers(this.events),
fields: {}, fields: {},
errors: [], errors: {},
}; };
}, },
computed: { computed: {
...@@ -105,21 +48,11 @@ export default { ...@@ -105,21 +48,11 @@ export default {
'formInitialData', 'formInitialData',
'formErrors', 'formErrors',
]), ]),
startEventOptions() {
return [ hasErrors() {
{ value: null, text: s__('CustomCycleAnalytics|Select start event') }, return (
...this.events.filter(isStartEvent).map(eventToOption), this.eventMismatchError || Object.values(this.errors).some(errArray => errArray?.length)
]; );
},
endEventOptions() {
const endEvents = getAllowedEndEvents(this.events, this.fields.startEventIdentifier);
return [
{ value: null, text: s__('CustomCycleAnalytics|Select stop event') },
...eventsByIdentifier(this.events, endEvents).map(eventToOption),
];
},
hasStartEvent() {
return this.fields.startEventIdentifier;
}, },
startEventRequiresLabel() { startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier); return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
...@@ -127,11 +60,6 @@ export default { ...@@ -127,11 +60,6 @@ export default {
endEventRequiresLabel() { endEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier); return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
}, },
hasErrors() {
return (
this.eventMismatchError || Object.values(this.errors).some(errArray => errArray?.length)
);
},
isComplete() { isComplete() {
if (this.hasErrors) { if (this.hasErrors) {
return false; return false;
...@@ -170,14 +98,10 @@ export default { ...@@ -170,14 +98,10 @@ export default {
return !endEvents.length || !endEvents.includes(endEventIdentifier); return !endEvents.length || !endEvents.includes(endEventIdentifier);
}, },
saveStageText() { saveStageText() {
return this.isEditingCustomStage return this.isEditingCustomStage ? I18N.BTN_UPDATE_STAGE : I18N.BTN_ADD_STAGE;
? s__('CustomCycleAnalytics|Update stage')
: s__('CustomCycleAnalytics|Add stage');
}, },
formTitle() { formTitle() {
return this.isEditingCustomStage return this.isEditingCustomStage ? I18N.TITLE_EDIT_STAGE : I18N.TITLE_ADD_STAGE;
? s__('CustomCycleAnalytics|Editing stage')
: s__('CustomCycleAnalytics|New stage');
}, },
hasHiddenStages() { hasHiddenStages() {
return this.hiddenStages.length; return this.hiddenStages.length;
...@@ -222,34 +146,26 @@ export default { ...@@ -222,34 +146,26 @@ export default {
this.$emit(STAGE_ACTIONS.CREATE, data); this.$emit(STAGE_ACTIONS.CREATE, data);
} }
}, },
handleSelectLabel(key, labelId) {
this.fields[key] = labelId;
},
handleClearLabel(key) {
this.fields[key] = null;
},
hasFieldErrors(key) { hasFieldErrors(key) {
return this.errors[key]?.length > 0; return this.errors[key]?.length > 0;
}, },
fieldErrorMessage(key) { fieldErrorMessage(key) {
return this.errors[key]?.join('\n'); return this.errors[key]?.join('\n');
}, },
onUpdateNameField() {
this.errors.name = DEFAULT_STAGE_NAMES.includes(this.fields.name.toLowerCase())
? [ERRORS.STAGE_NAME_EXISTS]
: [];
},
onUpdateStartEventField() {
this.fields.endEventIdentifier = null;
this.errors.endEventIdentifier = [ERRORS.INVALID_EVENT_PAIRS];
},
onUpdateEndEventField() {
this.errors.endEventIdentifier = [];
},
handleRecoverStage(id) { handleRecoverStage(id) {
this.$emit(STAGE_ACTIONS.UPDATE, { id, hidden: false }); this.$emit(STAGE_ACTIONS.UPDATE, { id, hidden: false });
}, },
handleUpdateFields(field, value) {
this.fields = { ...this.fields, [field]: value };
const newErrors = validateFields(this.fields);
newErrors.endEventIdentifier =
this.fields.startEventIdentifier && this.eventMismatchError
? [ERRORS.INVALID_EVENT_PAIRS]
: newErrors.endEventIdentifier;
this.errors = { ...this.errors, ...newErrors };
},
}, },
I18N,
}; };
</script> </script>
<template> <template>
...@@ -260,11 +176,13 @@ export default { ...@@ -260,11 +176,13 @@ export default {
<div class="gl-mb-1 gl-display-flex gl-justify-content-space-between gl-align-items-center"> <div class="gl-mb-1 gl-display-flex gl-justify-content-space-between gl-align-items-center">
<h4>{{ formTitle }}</h4> <h4>{{ formTitle }}</h4>
<gl-dropdown <gl-dropdown
:text="__('Recover hidden stage')" :text="$options.I18N.RECOVER_HIDDEN_STAGE"
class="js-recover-hidden-stage-dropdown" data-testid="recover-hidden-stage-dropdown"
right right
> >
<gl-dropdown-section-header>{{ __('Default stages') }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{
$options.I18N.RECOVER_STAGE_TITLE
}}</gl-dropdown-section-header>
<template v-if="hasHiddenStages"> <template v-if="hasHiddenStages">
<gl-dropdown-item <gl-dropdown-item
v-for="stage in hiddenStages" v-for="stage in hiddenStages"
...@@ -273,111 +191,30 @@ export default { ...@@ -273,111 +191,30 @@ export default {
>{{ stage.title }}</gl-dropdown-item >{{ stage.title }}</gl-dropdown-item
> >
</template> </template>
<p v-else class="mx-3 my-2">{{ __('All default stages are currently visible') }}</p> <p v-else class="mx-3 my-2">{{ $options.I18N.RECOVER_STAGES_VISIBLE }}</p>
</gl-dropdown> </gl-dropdown>
</div> </div>
<gl-form-group <custom-stage-form-fields
ref="name" :fields="fields"
:label="s__('CustomCycleAnalytics|Name')" :label-events="labelEvents"
label-for="custom-stage-name" :errors="errors"
:state="!hasFieldErrors('name')" :events="events"
:invalid-feedback="fieldErrorMessage('name')" @update="handleUpdateFields"
>
<gl-form-input
v-model="fields.name"
class="form-control"
type="text"
name="custom-stage-name"
:placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')"
required
@change.native="onUpdateNameField"
/>
</gl-form-group>
<div class="d-flex" :class="{ 'justify-content-between': startEventRequiresLabel }">
<div :class="[startEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group
ref="startEventIdentifier"
:label="s__('CustomCycleAnalytics|Start event')"
label-for="custom-stage-start-event"
:state="!hasFieldErrors('startEventIdentifier')"
:invalid-feedback="fieldErrorMessage('startEventIdentifier')"
>
<gl-form-select
v-model="fields.startEventIdentifier"
name="custom-stage-start-event"
:required="true"
:options="startEventOptions"
@change.native="onUpdateStartEventField"
/>
</gl-form-group>
</div>
<div v-if="startEventRequiresLabel" class="w-50 ml-1">
<gl-form-group
ref="startEventLabelId"
:label="s__('CustomCycleAnalytics|Start event label')"
label-for="custom-stage-start-event-label"
:state="!hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:selected-label-id="[fields.startEventLabelId]"
name="custom-stage-start-event-label"
@selectLabel="handleSelectLabel('startEventLabelId', $event)"
@clearLabel="handleClearLabel('startEventLabelId')"
/>
</gl-form-group>
</div>
</div>
<div class="d-flex" :class="{ 'justify-content-between': endEventRequiresLabel }">
<div :class="[endEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group
ref="endEventIdentifier"
:label="s__('CustomCycleAnalytics|Stop event')"
label-for="custom-stage-stop-event"
:state="!hasFieldErrors('endEventIdentifier')"
:invalid-feedback="fieldErrorMessage('endEventIdentifier')"
>
<gl-form-select
v-model="fields.endEventIdentifier"
name="custom-stage-stop-event"
:options="endEventOptions"
:required="true"
:disabled="!hasStartEvent"
@change.native="onUpdateEndEventField"
/>
</gl-form-group>
</div>
<div v-if="endEventRequiresLabel" class="w-50 ml-1">
<gl-form-group
ref="endEventLabelId"
:label="s__('CustomCycleAnalytics|Stop event label')"
label-for="custom-stage-stop-event-label"
:state="!hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:selected-label-id="[fields.endEventLabelId]"
name="custom-stage-stop-event-label"
@selectLabel="handleSelectLabel('endEventLabelId', $event)"
@clearLabel="handleClearLabel('endEventLabelId')"
/> />
</gl-form-group>
</div>
</div>
<div class="custom-stage-form-actions"> <div class="custom-stage-form-actions">
<gl-button <gl-button
:disabled="!isDirty" :disabled="!isDirty"
category="primary" category="primary"
class="js-save-stage-cancel" data-testid="cancel-custom-stage"
@click="handleCancel" @click="handleCancel"
> >
{{ __('Cancel') }} {{ $options.I18N.BTN_CANCEL }}
</gl-button> </gl-button>
<gl-button <gl-button
:disabled="!isComplete || !isDirty" :disabled="!isComplete || !isDirty"
variant="success" variant="success"
category="primary" category="primary"
class="js-save-stage" data-testid="save-custom-stage"
@click="handleSave" @click="handleSave"
> >
<gl-loading-icon v-if="isSavingCustomStage" size="sm" inline /> <gl-loading-icon v-if="isSavingCustomStage" size="sm" inline />
......
...@@ -32,7 +32,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -32,7 +32,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
start_event_identifier = :merge_request_created start_event_identifier = :merge_request_created
end_event_identifier = :merge_request_merged end_event_identifier = :merge_request_merged
start_label_event = :issue_label_added start_label_event = :issue_label_added
stop_label_event = :issue_label_removed end_label_event = :issue_label_removed
let(:add_stage_button) { '.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(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
...@@ -57,11 +57,11 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -57,11 +57,11 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
end end
def select_dropdown_option(name, value = start_event_identifier) def select_dropdown_option(name, value = start_event_identifier)
page.find("select[name='#{name}']").all('option').find { |item| item.value == value.to_s }.select_option page.find("[data-testid='#{name}'] select").all('option').find { |item| item.value == value.to_s }.select_option
end end
def select_dropdown_option_by_value(name, value, elem = 'option') def select_dropdown_option_by_value(name, value, elem = 'option')
page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option page.find("[data-testid='#{name}'] select").find("#{elem}[value=#{value}]").select_option
end end
def wait_for_labels(field) def wait_for_labels(field)
...@@ -212,17 +212,16 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -212,17 +212,16 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
before do before do
fill_in 'custom-stage-name', with: custom_stage_name fill_in 'custom-stage-name', with: custom_stage_name
select_dropdown_option 'custom-stage-start-event', start_event_identifier select_dropdown_option 'custom-stage-start-event', start_event_identifier
select_dropdown_option 'custom-stage-stop-event', end_event_identifier select_dropdown_option 'custom-stage-end-event', end_event_identifier
end end
it 'does not have label dropdowns' do it 'does not have label dropdowns' do
expect(page).not_to have_content(s_('CustomCycleAnalytics|Start event label')) expect(page).not_to have_content(s_('CustomCycleAnalytics|Start event label'))
expect(page).not_to have_content(s_('CustomCycleAnalytics|Stop event label')) expect(page).not_to have_content(s_('CustomCycleAnalytics|End event label'))
end end
it 'submit button is disabled if a default name is used' do it 'submit button is disabled if a default name is used' do
fill_in 'custom-stage-name', with: 'issue' fill_in 'custom-stage-name', with: 'issue'
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true) expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end end
...@@ -240,7 +239,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -240,7 +239,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
before do before do
fill_in 'custom-stage-name', with: custom_stage_with_labels_name fill_in 'custom-stage-name', with: custom_stage_with_labels_name
select_dropdown_option_by_value 'custom-stage-start-event', start_label_event select_dropdown_option_by_value 'custom-stage-start-event', start_label_event
select_dropdown_option_by_value 'custom-stage-stop-event', stop_label_event select_dropdown_option_by_value 'custom-stage-end-event', end_label_event
end end
it 'submit button is disabled' do it 'submit button is disabled' do
...@@ -249,11 +248,11 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -249,11 +248,11 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
context 'with labels available' do context 'with labels available' do
start_field = 'custom-stage-start-event-label' start_field = 'custom-stage-start-event-label'
end_field = 'custom-stage-stop-event-label' end_field = 'custom-stage-end-event-label'
it 'does not contain labels from outside the group' do it 'does not contain labels from outside the group' do
wait_for_labels(start_field) wait_for_labels(start_field)
menu = page.find("[name=#{start_field}] .dropdown-menu") menu = page.find("[data-testid=#{start_field}] .dropdown-menu")
expect(menu).not_to have_content(other_label.name) expect(menu).not_to have_content(other_label.name)
expect(menu).to have_content(first_label.name) expect(menu).to have_content(first_label.name)
...@@ -279,10 +278,10 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -279,10 +278,10 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
shared_examples 'can edit custom stages' do shared_examples 'can edit custom stages' do
context 'Edit stage form' do context 'Edit stage form' do
let(:stage_form_class) { '.custom-stage-form' } let(:stage_form_class) { '.custom-stage-form' }
let(:stage_save_button) { '.js-save-stage' } let(:stage_save_button) { '[data-testid="save-custom-stage"]' }
let(:name_field) { 'custom-stage-name' } let(:name_field) { 'custom-stage-name' }
let(:start_event_field) { 'custom-stage-start-event' } let(:start_event_field) { 'custom-stage-start-event' }
let(:end_event_field) { 'custom-stage-stop-event' } let(:end_event_field) { 'custom-stage-end-event' }
let(:updated_custom_stage_name) { 'Extra uber cool stage' } let(:updated_custom_stage_name) { 'Extra uber cool stage' }
before do before do
...@@ -413,7 +412,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -413,7 +412,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
open_recover_stage_dropdown open_recover_stage_dropdown
expect(page.find('.js-recover-hidden-stage-dropdown')).to have_text(s_('CycleAnalyticsStage|Issue')) expect(page.find("[data-testid='recover-hidden-stage-dropdown']")).to have_text(s_('CycleAnalyticsStage|Issue'))
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true displays a loading icon 1`] = ` exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true displays a loading icon 1`] = `
"<button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn js-save-stage btn-success btn-md disabled gl-button\\"> "<button data-testid=\\"save-custom-stage\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!----> <!---->
<!----> <span class=\\"gl-button-text\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" class=\\"align-text-bottom gl-spinner gl-spinner-dark gl-spinner-sm\\"></span></span> <!----> <span class=\\"gl-button-text\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" class=\\"align-text-bottom gl-spinner gl-spinner-dark gl-spinner-sm\\"></span></span>
Update stage Update stage
...@@ -9,7 +9,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display ...@@ -9,7 +9,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
`; `;
exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = ` exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
"<button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn js-save-stage btn-success btn-md disabled gl-button\\"> "<button data-testid=\\"save-custom-stage\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!----> <!---->
<!----> <span class=\\"gl-button-text\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" class=\\"align-text-bottom gl-spinner gl-spinner-dark gl-spinner-sm\\"></span></span> <!----> <span class=\\"gl-button-text\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" class=\\"align-text-bottom gl-spinner gl-spinner-dark gl-spinner-sm\\"></span></span>
Add stage Add stage
......
import { groupLabels, labelStartEvent, labelStopEvent } from '../../mock_data';
export const MERGE_REQUEST_CREATED = 'merge_request_created';
export const ISSUE_CREATED = 'issue_created';
export const MERGE_REQUEST_CLOSED = 'merge_request_closed';
export const ISSUE_CLOSED = 'issue_closed';
export const emptyState = {
id: null,
name: null,
startEventIdentifier: null,
startEventLabelId: null,
endEventIdentifier: null,
endEventLabelId: null,
};
export const emptyErrorsState = {
id: [],
name: [],
startEventIdentifier: [],
startEventLabelId: [],
endEventIdentifier: ['Please select a start event first'],
endEventLabelId: [],
};
export const firstLabel = groupLabels[0];
export const formInitialData = {
id: 74,
name: 'Cool stage pre',
startEventIdentifier: labelStartEvent.identifier,
startEventLabelId: firstLabel.id,
endEventIdentifier: labelStopEvent.identifier,
endEventLabelId: firstLabel.id,
};
export const minimumFields = {
name: 'Cool stage',
startEventIdentifier: MERGE_REQUEST_CREATED,
endEventIdentifier: MERGE_REQUEST_CLOSED,
};
import { initializeFormData } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/utils';
import { emptyErrorsState, emptyState, formInitialData } from './mock_data';
describe('initializeFormData', () => {
const checkInitializedData = (
{ emptyFieldState = emptyState, fields = {}, errors = emptyErrorsState },
{ fields: resultFields = emptyState, errors: resultErrors = emptyErrorsState },
) => {
const res = initializeFormData({ emptyFieldState, fields, errors });
expect(res.fields).toEqual(resultFields);
expect(res.errors).toMatchObject(resultErrors);
};
describe('without a startEventIdentifier', () => {
it('with no errors', () => {
checkInitializedData(
{ fields: {} },
{ errors: { endEventIdentifier: ['Please select a start event first'] } },
);
});
it('with field errors', () => {
const data = { errors: { name: ['is reserved'] } };
const result = {
errors: {
endEventIdentifier: ['Please select a start event first'],
name: ['is reserved'],
},
};
checkInitializedData(data, result);
});
});
describe('with a startEventIdentifier', () => {
it('with no errors', () => {
const data = {
fields: { startEventIdentifier: 'start-event' },
errors: { ...emptyErrorsState, endEventIdentifier: [] },
};
const result = {
fields: { ...emptyState, startEventIdentifier: 'start-event' },
errors: { ...emptyErrorsState, endEventIdentifier: [] },
};
checkInitializedData(data, result);
});
it('with field errors', () => {
const data = {
fields: { startEventIdentifier: 'start-event' },
errors: { name: ['is reserved'] },
};
const result = {
fields: { ...emptyState, startEventIdentifier: 'start-event' },
errors: { endEventIdentifier: [], name: ['is reserved'] },
};
checkInitializedData(data, result);
});
});
describe('with all fields set', () => {
it('with no errors', () => {
const data = { fields: formInitialData };
const result = { fields: formInitialData };
checkInitializedData(data, result);
});
it('with field errors', () => {
const data = { fields: formInitialData, errors: { name: ['is reserved'] } };
const result = { fields: formInitialData, errors: { name: ['is reserved'] } };
checkInitializedData(data, result);
});
});
});
...@@ -2,9 +2,8 @@ import { createLocalVue, mount } from '@vue/test-utils'; ...@@ -2,9 +2,8 @@ import { createLocalVue, mount } from '@vue/test-utils';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex'; import Vuex from 'vuex';
import CustomStageForm, { import CustomStageForm from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
initializeFormData, import { initializeFormData } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/utils';
} from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants'; import { STAGE_ACTIONS } from 'ee/analytics/cycle_analytics/constants';
import customStagesStore from 'ee/analytics/cycle_analytics/store/modules/custom_stages'; import customStagesStore from 'ee/analytics/cycle_analytics/store/modules/custom_stages';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -79,24 +78,32 @@ describe('CustomStageForm', () => { ...@@ -79,24 +78,32 @@ describe('CustomStageForm', () => {
const findEvent = ev => wrapper.emitted()[ev]; const findEvent = ev => wrapper.emitted()[ev];
const sel = { const sel = {
name: '[name="custom-stage-name"]', name: '[data-testid="custom-stage-name"] input',
startEvent: '[name="custom-stage-start-event"]', startEvent: '[data-testid="custom-stage-start-event"] select',
startEventLabel: '[name="custom-stage-start-event-label"]', startEventLabel: '[data-testid="custom-stage-start-event-label"]',
endEvent: '[name="custom-stage-stop-event"]', endEvent: '[data-testid="custom-stage-end-event"] select',
endEventLabel: '[name="custom-stage-stop-event-label"]', endEventLabel: '[data-testid="custom-stage-end-event-label"]',
submit: '.js-save-stage', submit: '[data-testid="save-custom-stage"]',
cancel: '.js-save-stage-cancel', cancel: '[data-testid="cancel-custom-stage"]',
invalidFeedback: '.invalid-feedback', invalidFeedback: '.invalid-feedback',
recoverStageDropdown: '.js-recover-hidden-stage-dropdown', recoverStageDropdown: '[data-testid="recover-hidden-stage-dropdown"]',
recoverStageDropdownTrigger: '.js-recover-hidden-stage-dropdown .dropdown-toggle', recoverStageDropdownTrigger: '[data-testid="recover-hidden-stage-dropdown"] .dropdown-toggle',
hiddenStageDropdownOption: '.js-recover-hidden-stage-dropdown .dropdown-item', hiddenStageDropdownOption: '[data-testid="recover-hidden-stage-dropdown"] .dropdown-item',
}; };
function getDropdownOption(_wrapper, dropdown, index) { function getDropdownOptions(_wrapper, dropdown) {
return _wrapper.find(dropdown).findAll('option');
}
function getDropdownOptionsArray(_wrapper, dropdown) {
return _wrapper return _wrapper
.find(dropdown) .find(dropdown)
.findAll('option') .findAll('option')
.at(index); .wrappers.map(w => w.attributes('value'));
}
function getDropdownOption(_wrapper, dropdown, index) {
return getDropdownOptions(_wrapper, dropdown).at(index);
} }
function selectDropdownOption(_wrapper, dropdown, index) { function selectDropdownOption(_wrapper, dropdown, index) {
...@@ -123,12 +130,12 @@ describe('CustomStageForm', () => { ...@@ -123,12 +130,12 @@ describe('CustomStageForm', () => {
}); });
} }
const findNameField = _wrapper => _wrapper.find({ ref: 'name' }); const findNameField = _wrapper => _wrapper.find('[data-testid="custom-stage-name"]');
const findNameFieldInput = _wrapper => _wrapper.find(sel.name); const findStartEventField = _wrapper => _wrapper.find('[data-testid="custom-stage-start-event"]');
function setNameField(_wrapper, value = '') { function setNameField(_wrapper, value = '') {
findNameFieldInput(_wrapper).setValue(value); wrapper.find(sel.name).setValue(value);
findNameFieldInput(_wrapper).trigger('change'); wrapper.find(sel.name).trigger('change');
return _wrapper.vm.$nextTick(); return _wrapper.vm.$nextTick();
} }
...@@ -148,7 +155,7 @@ describe('CustomStageForm', () => { ...@@ -148,7 +155,7 @@ describe('CustomStageForm', () => {
describe.each([ describe.each([
['Name', sel.name, true], ['Name', sel.name, true],
['Start event', sel.startEvent, true], ['Start event', sel.startEvent, true],
['Stop event', sel.endEvent, false], ['End event', sel.endEvent, false],
['Submit', sel.submit, false], ['Submit', sel.submit, false],
['Cancel', sel.cancel, false], ['Cancel', sel.cancel, false],
])('Default state', (field, $sel, enabledState) => { ])('Default state', (field, $sel, enabledState) => {
...@@ -275,7 +282,7 @@ describe('CustomStageForm', () => { ...@@ -275,7 +282,7 @@ describe('CustomStageForm', () => {
}); });
}); });
describe('Stop event', () => { describe('End event', () => {
const startEventArrayIndex = mergeRequestCreatedIndex; const startEventArrayIndex = mergeRequestCreatedIndex;
const startEventDropdownIndex = startEventArrayIndex + 1; const startEventDropdownIndex = startEventArrayIndex + 1;
const currAllowed = startEvents[startEventArrayIndex].allowedEndEvents; const currAllowed = startEvents[startEventArrayIndex].allowedEndEvents;
...@@ -307,7 +314,7 @@ describe('CustomStageForm', () => { ...@@ -307,7 +314,7 @@ describe('CustomStageForm', () => {
}); });
}); });
it('will update the list of stop events when a start event is changed', () => { it('will update the list of end events when a start event is changed', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const selectedStartEvent = startEvents[startEventDropdownIndex]; const selectedStartEvent = startEvents[startEventDropdownIndex];
expect(stopOptions).toHaveLength(1); expect(stopOptions).toHaveLength(1);
...@@ -322,11 +329,11 @@ describe('CustomStageForm', () => { ...@@ -322,11 +329,11 @@ describe('CustomStageForm', () => {
}); });
}); });
it('will display all the valid stop events', () => { it('will display all the valid end events', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const possibleEndEvents = stopEvents.filter(ev => currAllowed.includes(ev.identifier)); const possibleEndEvents = stopEvents.filter(ev => currAllowed.includes(ev.identifier));
expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>'); expect(stopOptions.at(0).html()).toEqual('<option value="">Select end event</option>');
selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex); selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex);
...@@ -339,11 +346,11 @@ describe('CustomStageForm', () => { ...@@ -339,11 +346,11 @@ describe('CustomStageForm', () => {
}); });
}); });
it('will not display stop events that are not in the list of allowed stop events', () => { it('will not display end events that are not in the list of allowed end events', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const excludedEndEvents = stopEvents.filter(ev => !currAllowed.includes(ev.identifier)); const excludedEndEvents = stopEvents.filter(ev => !currAllowed.includes(ev.identifier));
expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>'); expect(stopOptions.at(0).html()).toEqual('<option value="">Select end event</option>');
selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1); selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1);
...@@ -358,7 +365,7 @@ describe('CustomStageForm', () => { ...@@ -358,7 +365,7 @@ describe('CustomStageForm', () => {
}); });
}); });
describe('with a stop event selected and a change to the start event', () => { describe('with a end event selected and a change to the start event', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
...@@ -378,27 +385,29 @@ describe('CustomStageForm', () => { ...@@ -378,27 +385,29 @@ describe('CustomStageForm', () => {
}); });
it('notifies that a start event needs to be selected first', () => { it('notifies that a start event needs to be selected first', () => {
wrapper.setData({ fields: { startEventIdentifier: '' } });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.text()).toContain('Please select a start event first'); expect(wrapper.text()).toContain('Please select a start event first');
}); });
}); });
it('will notify if the current start and stop event pair is not valid', () => { it('will notify if the current start and end event pair is not valid', () => {
selectDropdownOption(wrapper, sel.startEvent, 2); selectDropdownOption(wrapper, sel.startEvent, 2);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(sel.invalidFeedback).exists()).toEqual(true); expect(wrapper.find(sel.invalidFeedback).exists()).toEqual(true);
expect(wrapper.find(sel.invalidFeedback).text()).toContain( expect(wrapper.find(sel.invalidFeedback).text()).toContain(
'Start event changed, please select a valid stop event', 'Start event changed, please select a valid end event',
); );
}); });
}); });
it('will update the list of stop events', () => { it('will update the list of end events', () => {
const se = wrapper.vm.endEventOptions; const preEndEvents = getDropdownOptionsArray(wrapper, sel.endEvent);
selectDropdownOption(wrapper, sel.startEvent, 2); selectDropdownOption(wrapper, sel.startEvent, 2);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(se[1].value).not.toEqual(wrapper.vm.endEventOptions[1].value); const opts = getDropdownOptionsArray(wrapper, sel.endEvent);
expect(preEndEvents).not.toEqual(opts);
}); });
}); });
...@@ -410,7 +419,7 @@ describe('CustomStageForm', () => { ...@@ -410,7 +419,7 @@ describe('CustomStageForm', () => {
}); });
}); });
describe('Stop event label', () => { describe('End event label', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -423,7 +432,7 @@ describe('CustomStageForm', () => { ...@@ -423,7 +432,7 @@ describe('CustomStageForm', () => {
expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false); expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false);
}); });
it('will display the stop event label field if a label event is selected', () => { it('will display the end event label field if a label event is selected', () => {
expect(wrapper.find(sel.endEventLabel).exists()).toEqual(false); expect(wrapper.find(sel.endEventLabel).exists()).toEqual(false);
wrapper.setData({ wrapper.setData({
...@@ -498,9 +507,12 @@ describe('CustomStageForm', () => { ...@@ -498,9 +507,12 @@ describe('CustomStageForm', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
wrapper.find(sel.name).setValue('Cool stage'); wrapper.find(sel.name).setValue('Cool stage');
return wrapper.vm return wrapper.vm.$nextTick().then(() =>
.$nextTick() setEventDropdowns({
.then(() => setEventDropdowns({ startEventDropdownIndex, stopEventDropdownIndex })); startEventDropdownIndex,
stopEventDropdownIndex,
}),
);
}); });
afterEach(() => { afterEach(() => {
...@@ -666,7 +678,9 @@ describe('CustomStageForm', () => { ...@@ -666,7 +678,9 @@ describe('CustomStageForm', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}) })
.then(() => { .then(() => {
expect(wrapper.vm.fields).toEqual({ ...formInitialData }); expect(wrapper.vm.fields).toEqual({
...formInitialData,
});
}); });
}); });
}); });
...@@ -788,9 +802,9 @@ describe('CustomStageForm', () => { ...@@ -788,9 +802,9 @@ describe('CustomStageForm', () => {
}); });
it('renders the errors for the relevant fields', () => { it('renders the errors for the relevant fields', () => {
expect(wrapper.find({ ref: 'name' }).html()).toContain('is reserved'); expect(findNameField(wrapper).html()).toContain('is reserved');
expect(wrapper.find({ ref: 'name' }).html()).toContain('cant be blank'); expect(findNameField(wrapper).html()).toContain('cant be blank');
expect(wrapper.find({ ref: 'startEventIdentifier' }).html()).toContain('cant be blank'); expect(findStartEventField(wrapper).html()).toContain('cant be blank');
}); });
}); });
...@@ -827,7 +841,13 @@ describe('CustomStageForm', () => { ...@@ -827,7 +841,13 @@ describe('CustomStageForm', () => {
wrapper = createComponent({ wrapper = createComponent({
stubs: formFieldStubs, stubs: formFieldStubs,
initialRootGetters: { initialRootGetters: {
hiddenStages: () => [{ id: 'my-stage', title: 'My default stage', hidden: true }], hiddenStages: () => [
{
id: 'my-stage',
title: 'My default stage',
hidden: true,
},
],
}, },
}); });
}); });
...@@ -913,7 +933,10 @@ describe('CustomStageForm', () => { ...@@ -913,7 +933,10 @@ describe('CustomStageForm', () => {
}, },
errors: {}, errors: {},
}); });
expect(res.fields).toEqual({ ...emptyFieldState, startEventIdentifier: 'start-event' }); expect(res.fields).toEqual({
...emptyFieldState,
startEventIdentifier: 'start-event',
});
expect(res.errors).toMatchObject({ expect(res.errors).toMatchObject({
endEventIdentifier: [], endEventIdentifier: [],
}); });
...@@ -929,7 +952,10 @@ describe('CustomStageForm', () => { ...@@ -929,7 +952,10 @@ describe('CustomStageForm', () => {
name: ['is reserved'], name: ['is reserved'],
}, },
}); });
expect(res.fields).toEqual({ ...emptyFieldState, startEventIdentifier: 'start-event' }); expect(res.fields).toEqual({
...emptyFieldState,
startEventIdentifier: 'start-event',
});
expect(res.errors).toMatchObject({ expect(res.errors).toMatchObject({
endEventIdentifier: [], endEventIdentifier: [],
name: ['is reserved'], name: ['is reserved'],
......
...@@ -2820,9 +2820,6 @@ msgstr "" ...@@ -2820,9 +2820,6 @@ msgstr ""
msgid "All changes are committed" msgid "All changes are committed"
msgstr "" msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits." msgid "All email addresses will be used to identify your commits."
msgstr "" msgstr ""
...@@ -8330,9 +8327,21 @@ msgstr "" ...@@ -8330,9 +8327,21 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage" msgid "CustomCycleAnalytics|Add stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|All default stages are currently visible"
msgstr ""
msgid "CustomCycleAnalytics|Default stages"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage" msgid "CustomCycleAnalytics|Editing stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|End event"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage" msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr "" msgstr ""
...@@ -8345,10 +8354,13 @@ msgstr "" ...@@ -8345,10 +8354,13 @@ msgstr ""
msgid "CustomCycleAnalytics|Please select a start event first" msgid "CustomCycleAnalytics|Please select a start event first"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Select start event" msgid "CustomCycleAnalytics|Recover hidden stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Select stop event" msgid "CustomCycleAnalytics|Select end event"
msgstr ""
msgid "CustomCycleAnalytics|Select start event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists" msgid "CustomCycleAnalytics|Stage name already exists"
...@@ -8357,18 +8369,12 @@ msgstr "" ...@@ -8357,18 +8369,12 @@ msgstr ""
msgid "CustomCycleAnalytics|Start event" msgid "CustomCycleAnalytics|Start event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Start event changed, please select a valid stop event" msgid "CustomCycleAnalytics|Start event changed, please select a valid end event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Start event label" msgid "CustomCycleAnalytics|Start event label"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Stop event"
msgstr ""
msgid "CustomCycleAnalytics|Stop event label"
msgstr ""
msgid "CustomCycleAnalytics|Update stage" msgid "CustomCycleAnalytics|Update stage"
msgstr "" msgstr ""
...@@ -8959,9 +8965,6 @@ msgstr "" ...@@ -8959,9 +8965,6 @@ msgstr ""
msgid "Default projects limit" msgid "Default projects limit"
msgstr "" msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Map a FogBugz account ID to a full name" msgid "Default: Map a FogBugz account ID to a full name"
msgstr "" 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