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