Commit 6d7bf430 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '11982-merge-trains-helper-text' into 'master'

Add merge train helper text to merge request widget

Closes #11982

See merge request gitlab-org/gitlab-ee!14097
parents 834efb6c 4b1413e5
......@@ -23,7 +23,7 @@ export default {
};
</script>
<template>
<section class="mr-widget-help">
<section class="mr-widget-help font-italic">
<template v-if="missingBranch">
{{ missingBranchInfo }}
</template>
......
......@@ -18,8 +18,8 @@ export default {
Deployment,
MrWidgetContainer,
MrWidgetPipeline,
MergeTrainInfo: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'),
MergeTrainPositionIndicator: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_position_indicator.vue'),
},
props: {
mr: {
......@@ -62,7 +62,7 @@ export default {
showVisualReviewAppLink() {
return this.mr.visualReviewAppAvailable;
},
showMergeTrainInfo() {
showMergeTrainPositionIndicator() {
return _.isNumber(this.mr.mergeTrainIndex);
},
},
......@@ -90,8 +90,8 @@ export default {
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
<merge-train-info
v-if="showMergeTrainInfo"
<merge-train-position-indicator
v-if="showMergeTrainPositionIndicator"
class="mr-widget-extension"
:merge-train-index="mr.mergeTrainIndex"
/>
......
......@@ -31,6 +31,9 @@ export default class MergeRequestStore {
this.targetBranchSha = data.target_branch_sha;
this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
......
......@@ -397,7 +397,6 @@
.mr-widget-help {
padding: 10px 16px 10px ($gl-padding-8 * 7);
font-style: italic;
}
.ci-coverage {
......@@ -906,7 +905,7 @@
}
.deploy-heading,
.merge-train-info {
.merge-train-position-indicator {
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
......
<script>
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
name: 'MergeTrainHelperText',
components: {
GlLink,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelineLink: {
type: String,
required: true,
},
mergeTrainWhenPipelineSucceedsDocsPath: {
type: String,
required: true,
},
mergeTrainLength: {
type: Number,
required: true,
},
},
computed: {
message() {
const text =
this.mergeTrainLength === 0
? s__(
'mrWidget|This merge request will start a merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds.',
)
: s__(
'mrWidget|This merge request will be added to the merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds.',
);
const sanitizedPipelineLink = _.escape(this.pipelineLink);
return sprintf(
text,
{
pipelineId: this.pipelineId,
linkStart: `<a class="js-pipeline-link" href="${sanitizedPipelineLink}">`,
linkEnd: '</a>',
},
false,
);
},
},
};
</script>
<template>
<section class="js-merge-train-helper-text mr-widget-help border-top">
<span v-html="message"></span>
<gl-link
:href="mergeTrainWhenPipelineSucceedsDocsPath"
target="_blank"
rel="noopener noreferrer"
class="js-documentation-link"
>
{{ s__('mrWidget|More information') }}
</gl-link>
</section>
</template>
......@@ -2,7 +2,7 @@
import { s__, sprintf } from '~/locale';
export default {
name: 'MergeTrainInfo',
name: 'MergeTrainPositionIndicator',
props: {
mergeTrainIndex: {
type: Number,
......@@ -20,7 +20,7 @@ export default {
</script>
<template>
<div class="pt-2 pb-2 pl-3 plr-3 merge-train-info">
<div class="pt-2 pb-2 pl-3 plr-3 merge-train-position-indicator">
<div class="media-body">
{{ message }}
</div>
......
<script>
import _ from 'underscore';
import ReportSection from '~/reports/components/report_section.vue';
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import GroupedMetricsReportsApp from 'ee/vue_shared/metrics_reports/grouped_metrics_reports_app.vue';
......@@ -11,9 +12,12 @@ import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import MrWidgetApprovals from './components/approvals/approvals.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import MergeTrainHelperText from './components/merge_train_helper_text.vue';
import { ATMTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
export default {
components: {
MergeTrainHelperText,
MrWidgetLicenses,
MrWidgetApprovals,
MrWidgetGeoSecondaryNode,
......@@ -131,6 +135,16 @@ export default {
performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
},
shouldRenderMergeTrainHelperText() {
return (
this.mr.pipeline &&
_.isNumber(this.mr.pipeline.id) &&
_.isString(this.mr.pipeline.path) &&
this.mr.preferredAutoMergeStrategy === ATMTWPS_MERGE_STRATEGY &&
!this.mr.autoMergeEnabled
);
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -331,6 +345,13 @@ export default {
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
:pipeline-id="mr.pipeline.id"
:pipeline-link="mr.pipeline.path"
:merge-train-length="mr.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
/>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
</div>
<mr-widget-pipeline-container
......
......@@ -41,6 +41,10 @@ module EE
end
end
def merge_train_when_pipeline_succeeds_docs_path
help_page_path('ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md', anchor: 'startadd-to-merge-train-when-pipeline-succeeds')
end
def target_project
merge_request.target_project.present(current_user: current_user)
end
......
......@@ -161,6 +161,9 @@ module EE
expose :api_unapprove_path do |merge_request|
presenter(merge_request).api_unapprove_path
end
expose :merge_train_when_pipeline_succeeds_docs_path do |merge_request|
presenter(merge_request).merge_train_when_pipeline_succeeds_docs_path
end
expose :blocking_merge_requests, if: -> (mr, _) { mr&.target_project&.feature_available?(:blocking_merge_requests) }
......
---
title: Add merge train helper text to merge request widget
merge_request: 14097
author:
type: added
......@@ -12,6 +12,8 @@ describe 'User adds to merge train when pipeline succeeds', :js do
target_project: project, target_branch: 'master')
end
let(:pipeline) { merge_request.all_pipelines.first }
before do
stub_licensed_features(merge_pipelines: true, merge_trains: true)
project.add_maintainer(user)
......@@ -21,10 +23,16 @@ describe 'User adds to merge train when pipeline succeeds', :js do
sign_in(user)
end
it 'shows Start merge train when pipeline succeeds button' do
it 'shows Start merge train when pipeline succeeds button and helper texts' do
visit project_merge_request_path(project, merge_request)
expect(page).to have_button('Start merge train when pipeline succeeds')
within('.js-merge-train-helper-text') do
expect(page).to have_content("This merge request will start a merge train when pipeline ##{pipeline.id} succeeds.")
expect(page).to have_link('More information',
href: MergeRequestPresenter.new(merge_request).merge_train_when_pipeline_succeeds_docs_path)
end
end
context 'when merge_trains EEP license is not available' do
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import MergeTrainHelperText from 'ee/vue_merge_request_widget/components/merge_train_helper_text.vue';
describe('MergeTrainHelperText', () => {
const localVue = createLocalVue();
let wrapper;
const factory = propsData => {
wrapper = shallowMount(localVue.extend(MergeTrainHelperText), {
propsData,
localVue,
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
});
it('should return the "start" version of the message if there is no existing merge train', () => {
factory({
pipelineId: 123,
pipelineLink: 'path/to/pipeline',
mergeTrainWhenPipelineSucceedsDocsPath: 'path/to/help',
mergeTrainLength: 0,
});
expect(trimText(wrapper.text())).toBe(
'This merge request will start a merge train when pipeline #123 succeeds. More information',
);
});
it('should render the correct pipeline link in the helper text', () => {
factory({
pipelineId: 123,
pipelineLink: 'path/to/pipeline',
mergeTrainWhenPipelineSucceedsDocsPath: 'path/to/help',
mergeTrainLength: 2,
});
const pipelineLink = wrapper.find('.js-pipeline-link').element;
expect(pipelineLink).toExist();
expect(pipelineLink.textContent).toContain('#123');
expect(pipelineLink).toHaveAttr('href', 'path/to/pipeline');
});
it('should sanitize the pipeline link', () => {
factory({
pipelineId: 123,
pipelineLink: '"></a> <script>console.log("hacked!!")</script> <a href="',
mergeTrainWhenPipelineSucceedsDocsPath: 'path/to/help',
mergeTrainLength: 2,
});
const pipelineLink = wrapper.find('.js-pipeline-link').element;
expect(pipelineLink).toExist();
// The escaped characters are un-escaped when rendered by the DOM,
// so we expect the value of the "href" attr to be exactly the same
// as the input. If the link was not sanitized, the "href" attr
// would equal "".
expect(pipelineLink).toHaveAttr(
'href',
'"></a> <script>console.log("hacked!!")</script> <a href="',
);
});
it('should render the correct documentation link in the helper text', () => {
factory({
pipelineId: 123,
pipelineLink: 'path/to/pipeline',
mergeTrainWhenPipelineSucceedsDocsPath: 'path/to/help',
mergeTrainLength: 2,
});
const docLink = wrapper.find(GlLink);
expect(docLink.exists()).toBe(true);
expect(docLink.attributes().href).toBe('path/to/help');
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper';
import MergeTrainInfo from 'ee/vue_merge_request_widget/components/merge_train_info.vue';
import { trimText } from 'helpers/text_helper';
import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/merge_train_position_indicator.vue';
describe('MergeTrainInfo', () => {
describe('MergeTrainPositionIndicator', () => {
const localVue = createLocalVue();
let wrapper;
let vm;
const factory = propsData => {
wrapper = shallowMount(localVue.extend(MergeTrainInfo), {
wrapper = shallowMount(localVue.extend(MergeTrainPositionIndicator), {
propsData,
localVue,
sync: false,
......
import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import { MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
import MergeTrainInfo from 'ee/vue_merge_request_widget/components/merge_train_info.vue';
import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/merge_train_position_indicator.vue';
import { mockStore } from 'spec/vue_mr_widget/mock_data';
describe('MrWidgetPipelineContainer', () => {
......@@ -29,7 +29,7 @@ describe('MrWidgetPipelineContainer', () => {
autoMergeStrategy: MT_MERGE_STRATEGY,
});
expect(wrapper.find(MergeTrainInfo).exists()).toBe(false);
expect(wrapper.find(MergeTrainPositionIndicator).exists()).toBe(false);
});
it('should not render the merge train indicator if the MR is closed', () => {
......@@ -38,7 +38,7 @@ describe('MrWidgetPipelineContainer', () => {
autoMergeStrategy: MT_MERGE_STRATEGY,
});
expect(wrapper.find(MergeTrainInfo).exists()).toBe(false);
expect(wrapper.find(MergeTrainPositionIndicator).exists()).toBe(false);
});
it('should not render the merge train indicator if the MR is not on the merge train', () => {
......@@ -47,7 +47,7 @@ describe('MrWidgetPipelineContainer', () => {
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.find(MergeTrainInfo).exists()).toBe(false);
expect(wrapper.find(MergeTrainPositionIndicator).exists()).toBe(false);
});
});
});
......@@ -29,6 +29,7 @@ import {
sastBaseAllIssues,
sastHeadAllIssues,
} from 'ee_spec/vue_shared/security_reports/mock_data';
import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
describe('ee merge request widget options', () => {
let vm;
......@@ -792,6 +793,20 @@ describe('ee merge request widget options', () => {
expect(vm.shouldRenderApprovals).toBeTruthy();
});
});
describe('shouldRenderMergeTrainHelperText', () => {
it('should return true if ATMTWPS is available and the user has not yet pressed the ATMTWPS button', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [ATMTWPS_MERGE_STRATEGY],
auto_merge_enabled: false,
},
});
expect(vm.shouldRenderMergeTrainHelperText).toBe(true);
});
});
});
describe('rendering source branch removal status', () => {
......@@ -889,6 +904,115 @@ describe('ee merge request widget options', () => {
});
});
describe('merge train helper text', () => {
const getHelperTextElement = () => vm.$el.querySelector('.js-merge-train-helper-text');
it('does not render the merge train helpe text if the ATMTWPS strategy is not available', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [MT_MERGE_STRATEGY],
pipeline: {
...mockData.pipeline,
active: true,
},
},
});
const helperText = getHelperTextElement();
expect(helperText).not.toExist();
});
it('renders the correct merge train helper text when there is an existing merge train', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [ATMTWPS_MERGE_STRATEGY],
merge_trains_count: 2,
merge_train_when_pipeline_succeeds_docs_path: 'path/to/help',
pipeline: {
...mockData.pipeline,
id: 123,
active: true,
},
},
});
const helperText = getHelperTextElement();
expect(helperText).toExist();
expect(helperText).toContainText(
'This merge request will be added to the merge train when pipeline #123 succeeds.',
);
});
it('renders the correct merge train helper text when there is no existing merge train', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [ATMTWPS_MERGE_STRATEGY],
merge_trains_count: 0,
merge_train_when_pipeline_succeeds_docs_path: 'path/to/help',
pipeline: {
...mockData.pipeline,
id: 123,
active: true,
},
},
});
const helperText = getHelperTextElement();
expect(helperText).toExist();
expect(helperText).toContainText(
'This merge request will start a merge train when pipeline #123 succeeds.',
);
});
it('renders the correct pipeline link inside the message', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [ATMTWPS_MERGE_STRATEGY],
merge_train_when_pipeline_succeeds_docs_path: 'path/to/help',
pipeline: {
...mockData.pipeline,
id: 123,
path: 'path/to/pipeline',
active: true,
},
},
});
const pipelineLink = getHelperTextElement().querySelector('.js-pipeline-link');
expect(pipelineLink).toExist();
expect(pipelineLink).toContainText('#123');
expect(pipelineLink).toHaveAttr('href', 'path/to/pipeline');
});
it('renders the documentation link inside the message', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
available_auto_merge_strategies: [ATMTWPS_MERGE_STRATEGY],
merge_train_when_pipeline_succeeds_docs_path: 'path/to/help',
pipeline: {
...mockData.pipeline,
active: true,
},
},
});
const pipelineLink = getHelperTextElement().querySelector('.js-documentation-link');
expect(pipelineLink).toExist();
expect(pipelineLink).toContainText('More information');
expect(pipelineLink).toHaveAttr('href', 'path/to/help');
});
});
describe('data', () => {
it('passes approval api paths to service', () => {
const paths = {
......
......@@ -18267,6 +18267,9 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
msgid "mrWidget|More information"
msgstr ""
msgid "mrWidget|No approval required"
msgstr ""
......@@ -18366,6 +18369,12 @@ msgstr ""
msgid "mrWidget|This merge request is in the process of being merged"
msgstr ""
msgid "mrWidget|This merge request will be added to the merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds."
msgstr ""
msgid "mrWidget|This merge request will start a merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds."
msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
......
......@@ -233,6 +233,8 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
merge_train_when_pipeline_succeeds_docs_path:
'/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds',
squash: true,
visual_review_app_available: true,
merge_trains_enabled: true,
......
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