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

Move stage actions to a separate component

Extracts the stage actions to a separate
component for simpler reuse

Refactor default stage field validation

Minor cleanup default stage fields, adds validation
for default stages and cleans up ui
parent c8e5ce10
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const NAME_MAX_LENGTH = 100;
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'),
FORM_TITLE: __('Create Value Stream'),
FORM_CREATED: __("'%{name}' Value Stream created"),
RECOVER_HIDDEN_STAGE: s__('CreateValueStreamForm|Recover hidden stage'),
RESTORE_HIDDEN_STAGE: s__('CreateValueStreamForm|Restore stage'),
RECOVER_STAGE_TITLE: s__('CreateValueStreamForm|Default stages'),
RECOVER_STAGES_VISIBLE: s__('CreateValueStreamForm|All default stages are currently visible'),
SELECT_START_EVENT: s__('CreateValueStreamForm|Select start event'),
SELECT_END_EVENT: s__('CreateValueStreamForm|Select end event'),
FORM_FIELD_NAME_LABEL: s__('CreateValueStreamForm|Name'),
FORM_FIELD_NAME_PLACEHOLDER: s__('CreateValueStreamForm|Enter a name for the stage'),
FIELD_STAGE_NAME_PLACEHOLDER: s__('CreateValueStreamForm|Enter stage name'),
FORM_FIELD_START_EVENT: s__('CreateValueStreamForm|Start event'),
FORM_FIELD_START_EVENT_LABEL: s__('CreateValueStreamForm|Start event label'),
FORM_FIELD_END_EVENT: s__('CreateValueStreamForm|End event'),
FORM_FIELD_END_EVENT_LABEL: s__('CreateValueStreamForm|End event label'),
DEFAULT_FIELD_START_EVENT_LABEL: s__('CreateValueStreamForm|Start event: '),
DEFAULT_FIELD_END_EVENT_LABEL: s__('CreateValueStreamForm|End event: '),
BTN_UPDATE_STAGE: s__('CreateValueStreamForm|Update stage'),
BTN_ADD_STAGE: s__('CreateValueStreamForm|Add stage'),
TITLE_EDIT_STAGE: s__('CreateValueStreamForm|Editing stage'),
TITLE_ADD_STAGE: s__('CreateValueStreamForm|New stage'),
BTN_CANCEL: __('Cancel'),
STAGE_INDEX: s__('CreateValueStreamForm|Stage %{index}'),
HIDDEN_DEFAULT_STAGE: s__('CreateValueStreamForm|%{name} (default)'),
};
export const ERRORS = {
START_EVENT_REQUIRED: s__('CustomCycleAnalytics|Please select a start event first'),
STAGE_NAME_EXISTS: s__('CustomCycleAnalytics|Stage name already exists'),
MIN_LENGTH: s__('CreateValueStreamForm|Name is required'),
MAX_LENGTH: sprintf(s__('CreateValueStreamForm|Maximum length %{maxLength} characters'), {
maxLength: NAME_MAX_LENGTH,
}),
START_EVENT_REQUIRED: s__('CreateValueStreamForm|Please select a start event first'),
STAGE_NAME_EXISTS: s__('CreateValueStreamForm|Stage name already exists'),
INVALID_EVENT_PAIRS: s__(
'CustomCycleAnalytics|Start event changed, please select a valid end event',
'CreateValueStreamForm|Start event changed, please select a valid end event',
),
};
export const STAGE_SORT_DIRECTION = {
UP: 'UP',
DOWN: 'DOWN',
};
export const defaultErrors = {
id: [],
name: [],
......@@ -44,3 +64,16 @@ export const defaultFields = {
endEventIdentifier: null,
endEventLabelId: null,
};
export const DEFAULT_STAGE_CONFIG = ['issue', 'plan', 'code', 'test', 'review', 'staging'].map(
id => ({
id,
name: capitalizeFirstCharacter(id),
startEventIdentifier: null,
endEventIdentifier: null,
startEventLabel: null,
endEventLabel: null,
custom: false,
hidden: false,
}),
);
......@@ -69,7 +69,7 @@ export default {
<template>
<div>
<gl-form-group
:label="$options.I18N.FORM_FIELD_NAME"
:label="$options.I18N.FORM_FIELD_NAME_LABEL"
label-for="custom-stage-name"
:state="hasFieldErrors('name')"
:invalid-feedback="fieldErrorMessage('name')"
......
<script>
import { GlFormGroup, GlFormInput, GlFormText } from '@gitlab/ui';
import StageFieldActions from './stage_field_actions.vue';
import { I18N } from './constants';
export default {
name: 'DefaultStageFields',
components: {
StageFieldActions,
GlFormGroup,
GlFormInput,
GlFormText,
},
props: {
index: {
type: Number,
required: true,
},
totalStages: {
type: Number,
required: true,
},
stage: {
type: Object,
required: true,
},
errors: {
type: Object,
required: false,
default: () => {},
},
},
methods: {
isValid(field) {
return !this.errors[field]?.length;
},
renderError(field) {
return this.errors[field]?.join('\n');
},
},
I18N,
};
</script>
<template>
<div class="gl-mb-4">
<div class="gl-display-flex">
<gl-form-group
class="gl-flex-grow-1 gl-mb-0"
:state="isValid('name')"
:invalid-feedback="renderError('name')"
>
<gl-form-input
v-model.trim="stage.name"
:name="`create-value-stream-stage-${index}`"
:placeholder="$options.I18N.FIELD_STAGE_NAME_PLACEHOLDER"
required
@input="$emit('input', $event)"
/>
</gl-form-group>
<stage-field-actions
:index="index"
:stage-count="totalStages"
@move="$emit('move', $event)"
@hide="$emit('hide', $event)"
/>
</div>
<div class="gl-display-flex" :data-testid="`stage-start-event-${index}`">
<span class="gl-m-0 gl-vertical-align-middle gl-mr-3 gl-font-weight-bold">{{
$options.I18N.DEFAULT_FIELD_START_EVENT_LABEL
}}</span>
<gl-form-text>{{ stage.startEventIdentifier }}</gl-form-text>
<gl-form-text v-if="stage.startEventLabel"
>&nbsp;-&nbsp;{{ stage.startEventLabel }}</gl-form-text
>
</div>
<div class="gl-display-flex" :data-testid="`stage-end-event-${index}`">
<span class="gl-m-0 gl-vertical-align-middle gl-mr-3 gl-font-weight-bold">{{
$options.I18N.DEFAULT_FIELD_START_EVENT_LABEL
}}</span>
<gl-form-text>{{ stage.endEventIdentifier }}</gl-form-text>
<gl-form-text v-if="stage.endEventLabel">&nbsp;-&nbsp;{{ stage.endEventLabel }}</gl-form-text>
</div>
</div>
</template>
<script>
import { GlButton, GlButtonGroup } from '@gitlab/ui';
import { STAGE_SORT_DIRECTION } from './constants';
export default {
name: 'StageFieldActions',
components: {
GlButton,
GlButtonGroup,
},
props: {
index: {
type: Number,
required: true,
},
stageCount: {
type: Number,
required: true,
},
},
computed: {
lastStageIndex() {
return this.stageCount - 1;
},
isFirstActiveStage() {
return this.index === 0;
},
isLastActiveStage() {
return this.index === this.lastStageIndex;
},
},
STAGE_SORT_DIRECTION,
};
</script>
<template>
<div>
<gl-button-group class="gl-px-2">
<gl-button
:data-testid="`stage-action-move-down-${index}`"
:disabled="isLastActiveStage"
icon="arrow-down"
@click="$emit('move', { index, direction: $options.STAGE_SORT_DIRECTION.DOWN })"
/>
<gl-button
:data-testid="`stage-action-move-up-${index}`"
:disabled="isFirstActiveStage"
icon="arrow-up"
@click="$emit('move', { index, direction: $options.STAGE_SORT_DIRECTION.UP })"
/>
</gl-button-group>
<gl-button
:data-testid="`stage-action-hide-${index}`"
icon="archive"
@click="$emit('hide', index)"
/>
</div>
</template>
import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from '@gitlab/ui';
import StageFieldActions from 'ee/analytics/cycle_analytics/components/create_value_stream_form/stage_field_actions.vue';
import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/default_stage_fields.vue';
let wrapper = null;
const defaultStageIndex = 0;
const totalStages = 5;
const stageNameError = 'Name is required';
const defaultErrors = { name: [stageNameError] };
const defaultStage = {
name: 'Cool new stage',
startEventIdentifier: 'some_start_event',
endEventIdentifier: 'some_end_event',
endEventLabel: 'some_label',
};
describe('DefaultStageFields', () => {
function createComponent({ stage = defaultStage, errors = {} } = {}) {
return shallowMount(DefaultStageFields, {
propsData: {
index: defaultStageIndex,
totalStages,
stage,
errors,
},
stubs: {
'labels-selector': false,
'gl-form-text': false,
},
});
}
const findStageFieldName = () => wrapper.find('[name="create-value-stream-stage-0"]');
const findStartEvent = () => wrapper.find('[data-testid="stage-start-event-0"]');
const findEndEvent = () => wrapper.find('[data-testid="stage-end-event-0"]');
const findFormGroup = () => wrapper.find(GlFormGroup);
const findFieldActions = () => wrapper.find(StageFieldActions);
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the stage field name', () => {
expect(findStageFieldName().exists()).toBe(true);
expect(findStageFieldName().html()).toContain(defaultStage.name);
});
it('renders the field start event', () => {
expect(findStartEvent().exists()).toBe(true);
expect(findStartEvent().html()).toContain(defaultStage.startEventIdentifier);
});
it('renders the field end event', () => {
const content = findEndEvent().html();
expect(content).toContain(defaultStage.endEventIdentifier);
expect(content).toContain(defaultStage.endEventLabel);
});
it('renders an event label if it exists', () => {
const content = findEndEvent().html();
expect(content).toContain(defaultStage.endEventLabel);
});
it('on field input emits an input event', () => {
expect(wrapper.emitted('input')).toBeUndefined();
const newInput = 'coooool';
findStageFieldName().vm.$emit('input', newInput);
expect(wrapper.emitted('input')[0]).toEqual([newInput]);
});
describe('StageFieldActions', () => {
it('when the stage is hidden emits a `hide` event', () => {
expect(wrapper.emitted('hide')).toBeUndefined();
const stageMoveParams = { index: defaultStageIndex, direction: 'UP' };
findFieldActions().vm.$emit('move', stageMoveParams);
expect(wrapper.emitted('move')[0]).toEqual([stageMoveParams]);
});
it('when the stage is moved emits a `move` event', () => {
expect(wrapper.emitted('move')).toBeUndefined();
findFieldActions().vm.$emit('move', defaultStageIndex);
expect(wrapper.emitted('move')[0]).toEqual([defaultStageIndex]);
});
});
describe('with field errors', () => {
beforeEach(() => {
wrapper = createComponent({ errors: defaultErrors });
});
it('displays the field error', () => {
expect(findFormGroup().html()).toContain(stageNameError);
});
});
});
import { shallowMount } from '@vue/test-utils';
import StageFieldActions from 'ee/analytics/cycle_analytics/components/create_value_stream_form/stage_field_actions.vue';
const defaultIndex = 0;
const stageCount = 3;
describe('StageFieldActions', () => {
function createComponent({ index = defaultIndex }) {
return shallowMount(StageFieldActions, {
propsData: {
index,
stageCount,
},
});
}
let wrapper = null;
const findMoveDownBtn = () => wrapper.find('[data-testid^="stage-action-move-down"]');
const findMoveUpBtn = () => wrapper.find('[data-testid^="stage-action-move-up"]');
const findHideBtn = () => wrapper.find('[data-testid^="stage-action-hide"]');
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('will render the move up action', () => {
expect(findMoveUpBtn().exists()).toBe(true);
});
it('will render the move down action', () => {
expect(findMoveDownBtn().exists()).toBe(true);
});
it('will render the hide action', () => {
expect(findHideBtn().exists()).toBe(true);
});
it('disables the move up button', () => {
expect(findMoveUpBtn().props('disabled')).toBe(true);
});
it('when the down button is clicked will emit a `move` event', () => {
findMoveDownBtn().vm.$emit('click');
expect(wrapper.emitted('move')[0]).toEqual([{ direction: 'DOWN', index: 0 }]);
});
it('when the up button is clicked will emit a `move` event', () => {
findMoveUpBtn().vm.$emit('click');
expect(wrapper.emitted('move')[0]).toEqual([{ direction: 'UP', index: 0 }]);
});
it('when the hide button is clicked will emit a `move` event', () => {
findHideBtn().vm.$emit('click');
expect(wrapper.emitted('hide')[0]).toEqual([0]);
});
describe('when the current index is the same as the total number of stages', () => {
beforeEach(() => {
wrapper = createComponent({ index: 2 });
});
it('disables the move down button', () => {
expect(findMoveDownBtn().props('disabled')).toBe(true);
});
});
});
......@@ -8180,6 +8180,84 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
msgid "CreateValueStreamForm|%{name} (default)"
msgstr ""
msgid "CreateValueStreamForm|Add stage"
msgstr ""
msgid "CreateValueStreamForm|All default stages are currently visible"
msgstr ""
msgid "CreateValueStreamForm|Default stages"
msgstr ""
msgid "CreateValueStreamForm|Editing stage"
msgstr ""
msgid "CreateValueStreamForm|End event"
msgstr ""
msgid "CreateValueStreamForm|End event label"
msgstr ""
msgid "CreateValueStreamForm|End event: "
msgstr ""
msgid "CreateValueStreamForm|Enter a name for the stage"
msgstr ""
msgid "CreateValueStreamForm|Enter stage name"
msgstr ""
msgid "CreateValueStreamForm|Maximum length %{maxLength} characters"
msgstr ""
msgid "CreateValueStreamForm|Name"
msgstr ""
msgid "CreateValueStreamForm|Name is required"
msgstr ""
msgid "CreateValueStreamForm|New stage"
msgstr ""
msgid "CreateValueStreamForm|Please select a start event first"
msgstr ""
msgid "CreateValueStreamForm|Recover hidden stage"
msgstr ""
msgid "CreateValueStreamForm|Restore stage"
msgstr ""
msgid "CreateValueStreamForm|Select end event"
msgstr ""
msgid "CreateValueStreamForm|Select start event"
msgstr ""
msgid "CreateValueStreamForm|Stage %{index}"
msgstr ""
msgid "CreateValueStreamForm|Stage name already exists"
msgstr ""
msgid "CreateValueStreamForm|Start event"
msgstr ""
msgid "CreateValueStreamForm|Start event changed, please select a valid end event"
msgstr ""
msgid "CreateValueStreamForm|Start event label"
msgstr ""
msgid "CreateValueStreamForm|Start event: "
msgstr ""
msgid "CreateValueStreamForm|Update stage"
msgstr ""
msgid "Created"
msgstr ""
......@@ -8357,57 +8435,21 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|All default stages are currently visible"
msgstr ""
msgid "CustomCycleAnalytics|Default stages"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|End event"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr ""
msgid "CustomCycleAnalytics|Name"
msgstr ""
msgid "CustomCycleAnalytics|New stage"
msgstr ""
msgid "CustomCycleAnalytics|Please select a start event first"
msgstr ""
msgid "CustomCycleAnalytics|Recover hidden stage"
msgstr ""
msgid "CustomCycleAnalytics|Select end event"
msgstr ""
msgid "CustomCycleAnalytics|Select start event"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event"
msgstr ""
msgid "CustomCycleAnalytics|Start event changed, please select a valid end event"
msgstr ""
msgid "CustomCycleAnalytics|Start event label"
msgstr ""
msgid "CustomCycleAnalytics|Update stage"
msgstr ""
msgid "Customer Portal"
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