Commit afa35fc4 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by David O'Regan

Adds some simple transitions for changing ui

Minor reorganization of some of the UI elements
to limit jumpiness in transitions
parent 97fbd3ee
...@@ -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