Commit a5be5a1b authored by Phil Hughes's avatar Phil Hughes

Merge branch '332593-confirmation-modal-merge-trains' into 'master'

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

See merge request gitlab-org/gitlab!68496
parents 3f6ce01f 28ff61b1
......@@ -28,6 +28,7 @@ import {
CONFIRM,
WARNING,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
......@@ -39,7 +40,6 @@ import CommitsHeader from './commits_header.vue';
import SquashBeforeMerge from './squash_before_merge.vue';
const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_FAILED_STATE = 'failed';
const PIPELINE_PENDING_STATE = 'pending';
const PIPELINE_SUCCESS_STATE = 'success';
......@@ -105,6 +105,10 @@ export default {
import(
'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: {
GlTooltip: GlTooltipDirective,
......@@ -125,6 +129,7 @@ export default {
squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
isPipelineFailedModalVisible: false,
};
},
computed: {
......@@ -327,7 +332,12 @@ export default {
: this.mr.commitMessageWithDescription;
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) {
this.isMergingImmediately = true;
}
......@@ -522,6 +532,11 @@ export default {
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
<merge-train-failed-pipeline-confirmation-dialog
:visible="isPipelineFailedModalVisible"
@startMergeTrain="onStartMergeTrainConfirmation"
@cancel="isPipelineFailedModalVisible = false"
/>
</gl-button-group>
<div
v-if="shouldShowMergeControls"
......
......@@ -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 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];
// SP - "Suggest Pipelines"
......
......@@ -38,5 +38,13 @@ export default {
pipelineId() {
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 { getIdFromGraphQLId } from '~/graphql_shared/utils';
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';
export const MERGE_DISABLED_TEXT_UNAPPROVED = __(
......@@ -76,5 +80,16 @@ export default {
isMergeImmediatelyDangerous() {
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 { mount, shallowMount } from '@vue/test-utils';
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 { 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';
......@@ -69,6 +70,7 @@ describe('ReadyToMerge', () => {
MergeTrainHelperText,
GlSprintf,
GlLink,
MergeTrainFailedPipelineConfirmationDialog,
},
});
......@@ -88,6 +90,8 @@ describe('ReadyToMerge', () => {
findMergeTrainHelperText().find('[data-testid="documentation-link"]');
const findFailedPipelineMergeTrainText = () =>
wrapper.find('[data-testid="failed-pipeline-merge-train-text"]');
const findMergeTrainFailedPipelineConfirmationDialog = () =>
wrapper.findComponent(MergeTrainFailedPipelineConfirmationDialog);
afterEach(() => {
if (wrapper?.destroy) {
......@@ -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', () => {
let dialog;
......
......@@ -4379,6 +4379,9 @@ msgstr ""
msgid "Are you sure you want to %{action} %{name}?"
msgstr ""
msgid "Are you sure you want to attempt to merge?"
msgstr ""
msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
......@@ -33337,6 +33340,9 @@ msgstr ""
msgid "The latest pipeline for this merge request did not complete successfully."
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."
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