Commit b6486b27 authored by David O'Regan's avatar David O'Regan

Merge branch '299408-follow-up-create-value-stream-form-ux-improvements' into 'master'

VSA - Create value stream form UX improvements

See merge request gitlab-org/gitlab!55133
parents de3c64af afa35fc4
......@@ -140,20 +140,23 @@ export default {
>
</gl-dropdown>
</gl-form-group>
<gl-form-group
v-if="startEventRequiresLabel"
class="gl-w-half gl-ml-2"
:data-testid="`custom-stage-start-event-label-${index}`"
:label="$options.i18n.FORM_FIELD_START_EVENT_LABEL"
:state="hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.startEventLabelId]"
:name="`custom-stage-start-label-${index}`"
@select-label="$emit('input', { field: 'startEventLabelId', value: $event })"
/>
</gl-form-group>
<div class="gl-w-half gl-ml-2">
<transition name="fade">
<gl-form-group
v-if="startEventRequiresLabel"
:data-testid="`custom-stage-start-event-label-${index}`"
:label="$options.i18n.FORM_FIELD_START_EVENT_LABEL"
:state="hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.startEventLabelId]"
:name="`custom-stage-start-label-${index}`"
@select-label="$emit('input', { field: 'startEventLabelId', value: $event })"
/>
</gl-form-group>
</transition>
</div>
</div>
<div class="gl-display-flex gl-justify-content-between">
<gl-form-group
......@@ -180,20 +183,23 @@ export default {
>
</gl-dropdown>
</gl-form-group>
<gl-form-group
v-if="endEventRequiresLabel"
class="gl-w-half gl-ml-2"
:data-testid="`custom-stage-end-event-label-${index}`"
:label="$options.i18n.FORM_FIELD_END_EVENT_LABEL"
:state="hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.endEventLabelId]"
:name="`custom-stage-end-label-${index}`"
@select-label="$emit('input', { field: 'endEventLabelId', value: $event })"
/>
</gl-form-group>
<div class="gl-w-half gl-ml-2">
<transition name="fade">
<gl-form-group
v-if="endEventRequiresLabel"
:data-testid="`custom-stage-end-event-label-${index}`"
:label="$options.i18n.FORM_FIELD_END_EVENT_LABEL"
:state="hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:selected-label-id="[stage.endEventLabelId]"
:name="`custom-stage-end-label-${index}`"
@select-label="$emit('input', { field: 'endEventLabelId', value: $event })"
/>
</gl-form-group>
</transition>
</div>
</div>
</div>
</template>
......@@ -86,25 +86,23 @@ export default {
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mt-2"
class="gl-display-flex gl-align-items-center gl-mt-3"
:data-testid="`stage-start-event-${index}`"
>
<span class="gl-m-0 gl-vertical-align-middle gl-mr-2 gl-font-weight-bold">{{
$options.i18n.DEFAULT_FIELD_START_EVENT_LABEL
}}</span>
<gl-form-text class="gl-m-0">{{ eventName(stage.startEventIdentifier) }}</gl-form-text>
<gl-form-text v-if="stage.startEventLabel" class="gl-m-0"
>&nbsp;-&nbsp;{{ stage.startEventLabel }}</gl-form-text
>
<gl-form-text class="gl-m-0" tag="span">{{
eventName(stage.startEventIdentifier)
}}</gl-form-text>
</div>
<div class="gl-display-flex gl-align-items-center" :data-testid="`stage-end-event-${index}`">
<span class="gl-m-0 gl-vertical-align-middle gl-mr-2 gl-font-weight-bold">{{
$options.i18n.DEFAULT_FIELD_END_EVENT_LABEL
}}</span>
<gl-form-text class="gl-m-0">{{ eventName(stage.endEventIdentifier) }}</gl-form-text>
<gl-form-text v-if="stage.endEventLabel" class="gl-m-0"
>&nbsp;-&nbsp;{{ stage.endEventLabel }}</gl-form-text
>
<gl-form-text class="gl-m-0" tag="span">{{
eventName(stage.endEventIdentifier)
}}</gl-form-text>
</div>
</div>
</template>
......@@ -44,7 +44,7 @@ export default {
return this.canRemove ? __('Remove') : __('Hide');
},
hideActionIcon() {
return this.canRemove ? 'remove' : 'archive';
return this.canRemove ? 'remove' : 'eye-slash';
},
hideActionTestId() {
return `stage-action-${this.canRemove ? 'remove' : 'hide'}-${this.index}`;
......
......@@ -243,12 +243,26 @@ export default {
]);
Vue.set(this, 'stages', [...this.stages, target]);
},
onAddStage() {
lastStage() {
const stages = this.$refs.formStages;
return stages[stages.length - 1];
},
async scrollToLastStage() {
await this.$nextTick();
// Scroll to the new stage we have added
this.lastStage().focus();
this.lastStage().scrollIntoView({ behavior: 'smooth' });
},
addNewStage() {
// validate previous stages only and add a new stage
this.validate();
Vue.set(this, 'stages', [...this.stages, { ...defaultCustomStageFields }]);
Vue.set(this, 'stageErrors', [...this.stageErrors, {}]);
},
onAddStage() {
this.addNewStage();
this.scrollToLastStage();
},
onFieldInput(activeStageIndex, { field, value }) {
const updatedStage = { ...this.stages[activeStageIndex], [field]: value };
Vue.set(this.stages, activeStageIndex, updatedStage);
......@@ -320,13 +334,15 @@ export default {
:state="isValueStreamNameValid"
required
/>
<gl-button
v-if="canRestore"
class="gl-ml-3"
variant="link"
@click="handleResetDefaults"
>{{ $options.i18n.RESTORE_DEFAULTS }}</gl-button
>
<transition name="fade">
<gl-button
v-if="canRestore"
class="gl-ml-3"
variant="link"
@click="handleResetDefaults"
>{{ $options.i18n.RESTORE_DEFAULTS }}</gl-button
>
</transition>
</div>
</gl-form-group>
<gl-form-radio-group
......@@ -339,7 +355,11 @@ export default {
@input="onSelectPreset"
/>
<div v-if="hasExtendedFormFields" data-testid="extended-form-fields">
<div v-for="(stage, activeStageIndex) in stages" :key="stageKey(activeStageIndex)">
<div
v-for="(stage, activeStageIndex) in stages"
ref="formStages"
:key="stageKey(activeStageIndex)"
>
<hr class="gl-my-3" />
<span
class="gl-display-flex gl-m-0 gl-vertical-align-middle gl-mr-2 gl-font-weight-bold gl-display-flex gl-pb-3"
......
import { GlFormGroup } from '@gitlab/ui';
import { GlFormGroup, GlFormInput, GlFormText } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/default_stage_fields.vue';
import StageFieldActions from 'ee/analytics/cycle_analytics/components/create_value_stream_form/stage_field_actions.vue';
......@@ -37,8 +37,11 @@ describe('DefaultStageFields', () => {
}
const findStageFieldName = () => wrapper.find('[name="create-value-stream-stage-0"]');
const findStageFieldNameInput = () => findStageFieldName().find(GlFormInput);
const findStartEvent = () => wrapper.find('[data-testid="stage-start-event-0"]');
const findStartEventInput = () => findStartEvent().find(GlFormText);
const findEndEvent = () => wrapper.find('[data-testid="stage-end-event-0"]');
const findEndEventInput = () => findEndEvent().find(GlFormText);
const findFormGroup = () => wrapper.find(GlFormGroup);
const findFieldActions = () => wrapper.find(StageFieldActions);
......@@ -52,28 +55,21 @@ describe('DefaultStageFields', () => {
});
it('renders the stage field name', () => {
expect(findStageFieldName().exists()).toBe(true);
expect(findStageFieldName().html()).toContain(defaultStage.name);
expect(findStageFieldNameInput().exists()).toBe(true);
expect(findStageFieldNameInput().html()).toContain(defaultStage.name);
});
it('disables input for the stage field name', () => {
expect(findStageFieldName().attributes('disabled')).toBe('disabled');
expect(findStageFieldNameInput().attributes('disabled')).toBe('disabled');
});
it('renders the field start event', () => {
expect(findStartEvent().exists()).toBe(true);
expect(findStartEvent().html()).toContain(ISSUE_CREATED.name);
expect(findStartEventInput().exists()).toBe(true);
expect(findStartEventInput().text()).toBe(ISSUE_CREATED.name);
});
it('renders the field end event', () => {
const content = findEndEvent().html();
expect(content).toContain(ISSUE_CLOSED.name);
expect(content).toContain(defaultStage.endEventLabel);
});
it('renders an event label if it exists', () => {
const content = findEndEvent().html();
expect(content).toContain(defaultStage.endEventLabel);
expect(findEndEventInput().text()).toBe(ISSUE_CLOSED.name);
});
it('does not emits any input', () => {
......@@ -107,7 +103,7 @@ describe('DefaultStageFields', () => {
});
it('displays the field error', () => {
expect(findFormGroup().html()).toContain(stageNameError);
expect(findFormGroup().attributes('invalid-feedback')).toBe(stageNameError);
});
});
});
......@@ -12,6 +12,9 @@ import {
} from '~/lib/utils/common_utils';
import { customStageEvents as formEvents, defaultStageConfig, rawCustomStage } from '../mock_data';
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -144,22 +147,31 @@ describe('ValueStreamForm', () => {
});
describe('Add stage button', () => {
beforeEach(() => {
wrapper = createComponent({
props: { hasExtendedFormFields: true },
stubs: {
CustomStageFields,
},
});
});
it('has the add stage button', () => {
expect(findBtn('actionSecondary')).toMatchObject({ text: 'Add another stage' });
});
it('adds a blank custom stage when clicked', () => {
expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length);
it('adds a blank custom stage when clicked', async () => {
expect(wrapper.vm.stages).toHaveLength(defaultStageConfig.length);
clickAddStage();
await clickAddStage();
expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length + 1);
});
it('validates existing fields when clicked', () => {
expect(wrapper.vm.nameError).toEqual([]);
it('validates existing fields when clicked', async () => {
expect(wrapper.vm.nameError).toHaveLength(0);
clickAddStage();
await clickAddStage();
expect(wrapper.vm.nameError).toEqual(['Name is required']);
});
......@@ -225,24 +237,37 @@ describe('ValueStreamForm', () => {
});
describe('Add stage button', () => {
beforeEach(() => {
wrapper = createComponent({
props: {
initialPreset,
initialData,
isEditing: true,
hasExtendedFormFields: true,
},
stubs: {
CustomStageFields,
},
});
});
it('has the add stage button', () => {
expect(findBtn('actionSecondary')).toMatchObject({ text: 'Add another stage' });
});
it('adds a blank custom stage when clicked', () => {
it('adds a blank custom stage when clicked', async () => {
expect(wrapper.vm.stages.length).toBe(stageCount);
clickAddStage();
await clickAddStage();
expect(wrapper.vm.stages.length).toBe(stageCount + 1);
});
it('validates existing fields when clicked', () => {
it('validates existing fields when clicked', async () => {
expect(wrapper.vm.nameError).toEqual([]);
wrapper.findByTestId('create-value-stream-name').find(GlFormInput).vm.$emit('input', '');
clickAddStage();
await clickAddStage();
expect(wrapper.vm.nameError).toEqual(['Name is required']);
});
......
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