Commit 28ff61b1 authored by Payton Burdette's avatar Payton Burdette Committed by Phil Hughes

Add confirmation modal when pipeline failed [RUN AS-IF-FOSS]

parent 3e840e6e
...@@ -28,6 +28,7 @@ import { ...@@ -28,6 +28,7 @@ import {
CONFIRM, CONFIRM,
WARNING, WARNING,
MT_MERGE_STRATEGY, MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
} from '../../constants'; } from '../../constants';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
...@@ -39,7 +40,6 @@ import CommitsHeader from './commits_header.vue'; ...@@ -39,7 +40,6 @@ import CommitsHeader from './commits_header.vue';
import SquashBeforeMerge from './squash_before_merge.vue'; import SquashBeforeMerge from './squash_before_merge.vue';
const PIPELINE_RUNNING_STATE = 'running'; const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_FAILED_STATE = 'failed';
const PIPELINE_PENDING_STATE = 'pending'; const PIPELINE_PENDING_STATE = 'pending';
const PIPELINE_SUCCESS_STATE = 'success'; const PIPELINE_SUCCESS_STATE = 'success';
...@@ -105,6 +105,10 @@ export default { ...@@ -105,6 +105,10 @@ export default {
import( import(
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue' 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
), ),
MergeTrainFailedPipelineConfirmationDialog: () =>
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -125,6 +129,7 @@ export default { ...@@ -125,6 +129,7 @@ export default {
squashBeforeMerge: this.mr.squashIsSelected, squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly, isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage, squashCommitMessage: this.mr.squashCommitMessage,
isPipelineFailedModalVisible: false,
}; };
}, },
computed: { computed: {
...@@ -327,7 +332,12 @@ export default { ...@@ -327,7 +332,12 @@ export default {
: this.mr.commitMessageWithDescription; : this.mr.commitMessageWithDescription;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
}, },
handleMergeButtonClick(useAutoMerge, mergeImmediately = false) { handleMergeButtonClick(useAutoMerge, mergeImmediately = false, confirmationClicked = false) {
if (this.showFailedPipelineModal && !confirmationClicked) {
this.isPipelineFailedModalVisible = true;
return;
}
if (mergeImmediately) { if (mergeImmediately) {
this.isMergingImmediately = true; this.isMergingImmediately = true;
} }
...@@ -522,6 +532,11 @@ export default { ...@@ -522,6 +532,11 @@ export default {
@mergeImmediately="onMergeImmediatelyConfirmation" @mergeImmediately="onMergeImmediatelyConfirmation"
/> />
</gl-dropdown> </gl-dropdown>
<merge-train-failed-pipeline-confirmation-dialog
:visible="isPipelineFailedModalVisible"
@startMergeTrain="onStartMergeTrainConfirmation"
@cancel="isPipelineFailedModalVisible = false"
/>
</gl-button-group> </gl-button-group>
<div <div
v-if="shouldShowMergeControls" v-if="shouldShowMergeControls"
......
...@@ -10,6 +10,8 @@ export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds'; ...@@ -10,6 +10,8 @@ export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds'; export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train'; export const MT_MERGE_STRATEGY = 'merge_train';
export const PIPELINE_FAILED_STATE = 'failed';
export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY]; export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
// SP - "Suggest Pipelines" // SP - "Suggest Pipelines"
......
...@@ -38,5 +38,13 @@ export default { ...@@ -38,5 +38,13 @@ export default {
pipelineId() { pipelineId() {
return this.pipeline.id; return this.pipeline.id;
}, },
showFailedPipelineModal() {
return false;
},
},
methods: {
onStartMergeTrainConfirmation() {
return false;
},
}, },
}; };
<script>
import { GlModal, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
name: 'MergeTrainFailedPipelineConfirmationDialog',
i18n: {
title: __('Start merge train'),
cancel: __('Cancel'),
info: __('The latest pipeline for this merge request has failed.'),
confirmation: __('Are you sure you want to attempt to merge?'),
},
components: {
GlModal,
GlButton,
},
props: {
visible: {
type: Boolean,
required: true,
},
},
methods: {
hide() {
this.$refs.modal.hide();
},
cancel() {
this.hide();
this.$emit('cancel');
},
focusCancelButton() {
this.$refs.cancelButton.$el.focus();
},
startMergeTrain() {
this.$emit('startMergeTrain');
this.hide();
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="merge-train-failed-pipeline-confirmation-dialog"
size="sm"
:title="$options.i18n.title"
:visible="visible"
@shown="focusCancelButton"
@hide="$emit('cancel')"
>
<p>{{ $options.i18n.info }}</p>
<p>{{ $options.i18n.confirmation }}</p>
<template #modal-footer>
<gl-button ref="cancelButton" @click="cancel">{{ $options.i18n.cancel }}</gl-button>
<gl-button variant="danger" data-testid="start-merge-train" @click="startMergeTrain">
{{ $options.i18n.title }}
</gl-button>
</template>
</gl-modal>
</template>
import { isNumber, isString } from 'lodash'; import { isNumber, isString } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; import {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
} from '~/vue_merge_request_widget/constants';
import base from '~/vue_merge_request_widget/mixins/ready_to_merge'; import base from '~/vue_merge_request_widget/mixins/ready_to_merge';
export const MERGE_DISABLED_TEXT_UNAPPROVED = __( export const MERGE_DISABLED_TEXT_UNAPPROVED = __(
...@@ -76,5 +80,16 @@ export default { ...@@ -76,5 +80,16 @@ export default {
isMergeImmediatelyDangerous() { isMergeImmediatelyDangerous() {
return [MT_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY].includes(this.preferredAutoMergeStrategy); return [MT_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY].includes(this.preferredAutoMergeStrategy);
}, },
showFailedPipelineModal() {
const pipelineFailed = this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed;
const mergeStrateyMergeTrain = this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY;
return pipelineFailed && mergeStrateyMergeTrain;
},
},
methods: {
onStartMergeTrainConfirmation() {
this.handleMergeButtonClick(this.isAutoMergeAvailable, false, true);
},
}, },
}; };
import { shallowMount } from '@vue/test-utils';
import MergeTrainFailedPipelineConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue';
import { trimText } from 'helpers/text_helper';
describe('MergeTrainFailedPipelineConfirmationDialog', () => {
let wrapper;
const GlModal = {
template: `
<div>
<slot></slot>
<slot name="modal-footer"></slot>
</div>
`,
methods: {
hide: jest.fn(),
},
};
const createComponent = () => {
wrapper = shallowMount(MergeTrainFailedPipelineConfirmationDialog, {
propsData: {
visible: true,
},
stubs: {
GlModal,
},
attachTo: document.body,
});
};
const findModal = () => wrapper.find(GlModal);
const findStartMergeTrainBtn = () => wrapper.find('[data-testid="start-merge-train"]');
const findCancelBtn = () => wrapper.find({ ref: 'cancelButton' });
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('should render informational text explaining why merging immediately can be dangerous', () => {
expect(trimText(wrapper.text())).toContain(
'The latest pipeline for this merge request has failed. Are you sure you want to attempt to merge?',
);
});
it('should emit the startMergeTrain event', () => {
findStartMergeTrainBtn().vm.$emit('click');
expect(wrapper.emitted('startMergeTrain')).toBeTruthy();
});
it('when the cancel button is clicked should emit cancel and call hide', () => {
jest.spyOn(findModal().vm, 'hide');
findCancelBtn().vm.$emit('click');
expect(wrapper.emitted('cancel')).toBeTruthy();
expect(findModal().vm.hide).toHaveBeenCalled();
});
it('should emit cancel when the hide event is emitted', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted('cancel')).toBeTruthy();
});
it('when modal is shown it will focus the cancel button', () => {
findCancelBtn().element.focus = jest.fn();
findModal().vm.$emit('shown');
expect(findCancelBtn().element.focus).toHaveBeenCalled();
});
});
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import MergeImmediatelyConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'; import MergeImmediatelyConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue';
import MergeTrainFailedPipelineConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue';
import MergeTrainHelperText from 'ee/vue_merge_request_widget/components/merge_train_helper_text.vue'; import MergeTrainHelperText from 'ee/vue_merge_request_widget/components/merge_train_helper_text.vue';
import { MERGE_DISABLED_TEXT_UNAPPROVED } from 'ee/vue_merge_request_widget/mixins/ready_to_merge'; import { MERGE_DISABLED_TEXT_UNAPPROVED } from 'ee/vue_merge_request_widget/mixins/ready_to_merge';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
...@@ -69,6 +70,7 @@ describe('ReadyToMerge', () => { ...@@ -69,6 +70,7 @@ describe('ReadyToMerge', () => {
MergeTrainHelperText, MergeTrainHelperText,
GlSprintf, GlSprintf,
GlLink, GlLink,
MergeTrainFailedPipelineConfirmationDialog,
}, },
}); });
...@@ -88,6 +90,8 @@ describe('ReadyToMerge', () => { ...@@ -88,6 +90,8 @@ describe('ReadyToMerge', () => {
findMergeTrainHelperText().find('[data-testid="documentation-link"]'); findMergeTrainHelperText().find('[data-testid="documentation-link"]');
const findFailedPipelineMergeTrainText = () => const findFailedPipelineMergeTrainText = () =>
wrapper.find('[data-testid="failed-pipeline-merge-train-text"]'); wrapper.find('[data-testid="failed-pipeline-merge-train-text"]');
const findMergeTrainFailedPipelineConfirmationDialog = () =>
wrapper.findComponent(MergeTrainFailedPipelineConfirmationDialog);
afterEach(() => { afterEach(() => {
if (wrapper?.destroy) { if (wrapper?.destroy) {
...@@ -323,6 +327,32 @@ describe('ReadyToMerge', () => { ...@@ -323,6 +327,32 @@ describe('ReadyToMerge', () => {
}); });
}); });
describe('merge train failed confirmation dialog', () => {
it.each`
mergeStrategy | isPipelineFailed | isVisible
${MT_MERGE_STRATEGY} | ${true} | ${true}
${MT_MERGE_STRATEGY} | ${false} | ${false}
${MTWPS_MERGE_STRATEGY} | ${true} | ${false}
${MWPS_MERGE_STRATEGY} | ${true} | ${false}
`(
'with merge stragtegy $mergeStrategy and pipeline failed status of $isPipelineFailed we should show the modal: $isVisible',
async ({ mergeStrategy, isPipelineFailed, isVisible }) => {
factory({ preferredAutoMergeStrategy: mergeStrategy, isPipelineFailed });
const modalConfirmation = findMergeTrainFailedPipelineConfirmationDialog();
if (!isVisible) {
// need to mock if we don't show modal
// to prevent internals from being invoked
vm.handleMergeButtonClick = jest.fn();
}
await findMergeButton().vm.$emit('click');
expect(modalConfirmation.props('visible')).toBe(isVisible);
},
);
});
describe('merge immediately warning dialog', () => { describe('merge immediately warning dialog', () => {
let dialog; let dialog;
......
...@@ -4379,6 +4379,9 @@ msgstr "" ...@@ -4379,6 +4379,9 @@ msgstr ""
msgid "Are you sure you want to %{action} %{name}?" msgid "Are you sure you want to %{action} %{name}?"
msgstr "" msgstr ""
msgid "Are you sure you want to attempt to merge?"
msgstr ""
msgid "Are you sure you want to cancel editing this comment?" msgid "Are you sure you want to cancel editing this comment?"
msgstr "" msgstr ""
...@@ -33322,6 +33325,9 @@ msgstr "" ...@@ -33322,6 +33325,9 @@ msgstr ""
msgid "The latest pipeline for this merge request did not complete successfully." msgid "The latest pipeline for this merge request did not complete successfully."
msgstr "" msgstr ""
msgid "The latest pipeline for this merge request has failed."
msgstr ""
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc." msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
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