Commit 6e969c53 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets Committed by Bryce Johnson
Browse files

Tmp MR Widget base for approvals.

parent 8f93280e
......@@ -85,6 +85,7 @@ import Vue from 'vue';
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
} else {
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
}
......
......@@ -51,6 +51,7 @@ Vue.use(VueResource);
}
discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
} else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
}
......
......@@ -43,7 +43,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
// ci_status_url - String, URL to use to check CI status
//
this.opts = opts;
this.$widgetBody = $('.mr-widget-body');
this.$widgetBody = $('.mr-widget-body:eq(0)');
$('#modal_merge_info').modal({
show: false
});
......@@ -111,7 +111,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
return $('.mr-widget-body:eq(0)').html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
......@@ -130,11 +130,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
};
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, (data) => {
var that = this;
return $.get(this.opts.merge_check_url, function(data) {
var $html = $(data);
this.updateMergeButton(this.status, this.hasCi, $html);
$('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
$('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
that.updateMergeButton(this.status, this.hasCi, $html);
$('.mr-widget-body:eq(0)').replaceWith($html.find('.mr-widget-body'));
$('.mr-widget-footer:eq(0)').replaceWith($html.find('.mr-widget-footer'));
});
};
......@@ -159,15 +160,15 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha ||
data.pipeline !== _this.opts.ci_pipeline) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
if (data.pipeline) {
_this.opts.ci_pipeline = data.pipeline;
_this.updatePipelineUrls(data.pipeline);
......@@ -233,8 +234,8 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
if (state == null) {
return;
}
$('.ci_widget').hide();
$('.ci_widget.ci-' + state).show();
$('.ci_widget:eq(0)').hide();
$('.ci_widget.ci-' + state).eq(0).show();
this.initMiniPipelineGraph();
};
......
......@@ -377,6 +377,7 @@ require('./task_list');
}
gl.utils.localTimeAgo($('.js-timeago'), false);
gl.mrWidget.checkStatus();
return this.updateNotesCount(1);
};
......@@ -683,6 +684,9 @@ require('./task_list');
return note.remove();
};
})(this));
gl.mrWidget.checkStatus();
// Decrement the "Discussions" counter only once
return this.updateNotesCount(-1);
};
......
export default {
name: 'MRWidgetAuthor',
props: {
author: { type: Object, required: true },
},
template: `
<a class="author_link" :href="author.webUrl">
<img :src="author.avatarUrl" class="avatar avatar-inline s16" />
<span class="author">{{author.name}}</span>
</a>
`,
};
import MRWidgetAuthor from './mr_widget_author';
export default {
name: 'MRWidgetAuthorTime',
props: {
actionText: { type: String, required: true },
author: { type: Object, required: true },
dateTitle: { type: String, required: true },
dateReadable: { type: String, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
template: `
<h4>
{{actionText}}
<mr-widget-author :author="author" />
<time :title='dateTitle' data-toggle="tooltip" data-placement="top" data-container="body">
{{dateReadable}}
</time>
</h4>
`,
};
import '~/lib/utils/datetime_utility';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
export default {
name: 'MRWidgetDeployment',
props: {
mr: { type: Object, required: true },
},
computed: {
svg() {
return statusClassToSvgMap.icon_status_success;
},
},
methods: {
formatDate(date) {
return gl.utils.getTimeago().format(date);
},
hasExternalUrls(deployment = {}) {
return deployment.external_url && deployment.external_url_formatted;
},
hasDeploymentTime(deployment = {}) {
return deployment.deployed_at && deployment.deployed_at_formatted;
},
hasDeploymentMeta(deployment = {}) {
return deployment.url && deployment.name;
},
stopEnvironment(deployment) {
const msg = 'Are you sure you want to stop this environment?';
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
// TODO: Handle deployment cancel when backend is implemented.
}
},
},
template: `
<div class="mr-widget-heading">
<div class="ci_widget" v-for="deployment in mr.deployments">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
<span v-html="svg" aria-hidden="true"></span>
</span>
</div>
<span>
<span v-if="hasDeploymentMeta(deployment)">Deployed to</span>
<a
v-if="hasDeploymentMeta(deployment)"
:href="deployment.url"
target="_blank" rel="noopener noreferrer nofollow" class="js-deploy-meta">
{{deployment.name}}
</a>
<span v-if="hasExternalUrls(deployment)">on</span>
<a
v-if="hasExternalUrls(deployment)"
:href="deployment.external_url"
target="_blank" rel="noopener noreferrer nofollow" class="js-deploy-url">
{{deployment.external_url_formatted}}
</a>
<span
v-if="hasDeploymentTime(deployment)"
:data-title="deployment.deployed_at_formatted"
class="js-deploy-time" data-toggle="tooltip" data-placement="top">
{{formatDate(deployment.deployed_at)}}
</span>
<button
v-if="deployment.stop_url"
@click="stopEnvironment(deployment)"
class="btn btn-default btn-xs" type="button">
Stop environment
</button>
</span>
</div>
</div>
`,
};
require('../../lib/utils/text_utility');
export default {
name: 'MRWidgetHeader',
props: {
mr: { type: Object, required: true },
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
},
},
template: `
<div class="mr-source-target">
<div class="pull-right" v-if="mr.isOpen">
<a href="#modal_merge_info" data-toggle="modal" class="btn inline btn-grouped btn-sm">Check out branch</a>
<span class="dropdown inline prepend-left-5">
<a class="btn btn-sm dropdown-toggle" data-toggle="dropdown">
Download as <i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a :href="mr.emailPatchesPath">Email patches</a>
</li>
<li>
<a :href="mr.plainDiffPath">Plain diff</a>
</li>
</ul>
</span>
</div>
<div class="normal">
<span>Request to merge</span>
<span class="label-branch">{{mr.sourceBranch}}</span>
<span>into</span>
<span class="label-branch">
<a href="#">{{mr.targetBranch}}</a>
</span>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
({{mr.divergedCommitsCount}} {{commitsText}} behind)
</span>
</div>
</div>
`,
};
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: { type: String, required: false, default: '' },
},
template: `
<section class="mr-widget-help">
<template v-if="missingBranch">If the {{missingBranch}} branch exists in your local repository, you</template>
<template v-else>You</template>
can merge this merge request manually using the
<a data-toggle="modal" href="#modal_merge_info">command line</a>
</section>
`,
};
import PipelineStage from '../../vue_pipelines_index/components/stage';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
export default {
name: 'MRWidgetPipeline',
props: {
mr: { type: Object, required: true },
},
components: {
'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon,
},
computed: {
hasCIError() {
const { hasCI, ciStatus } = this.mr;
return hasCI && !ciStatus;
},
svg() {
return statusClassToSvgMap.icon_status_failed;
},
},
template: `
<div class="mr-widget-heading">
<div class="ci_widget">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed js-ci-error">
<span class="js-icon-link icon-link">
<span v-html="svg" aria-hidden="true"></span>
</span>
</div>
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipeline.details.status" />
<span>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}}
</span>
<div class="mr-widget-pipeline-graph">
<div class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="mr.pipeline.details.stages.length > 0"
v-for="stage in mr.pipeline.details.stages">
<pipeline-stage :stage="stage" />
</div>
</div>
</div>
<span>
for
<a class="monospace js-commit-link"
:href="mr.pipeline.commit.commit_path">{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%
</span>
</template>
</div>
</div>
`,
};
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
},
computed: {
hasLinks() {
return this.relatedLinks.closing || this.relatedLinks.mentioned;
},
},
methods: {
hasMultipleIssues(text) {
return !text ? false : text.match(/<\/a> and <a/);
},
issueLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
},
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
},
},
template: `
<section class="mr-info-list mr-links" v-if="hasLinks">
<div class="legend"></div>
<p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}} <span v-html="relatedLinks.closing"></span>.
</p>
<p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span>
<span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed.
</p>
</section>
`,
};
export default {
name: 'MRWidgetArchived',
template: `
<div class="mr-widget-body">
<button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
<span class="bold">This project is archived, write access has been disabled.</span>
</div>
`,
};
export default {
name: 'MRWidgetChecking',
template: `
<div class="mr-widget-body">
<button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
<span class="bold">
Checking ability to merge automatically.
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</span>
</div>
`,
};
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
export default {
name: 'MRWidgetClosed',
props: {
mr: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
},
template: `
<div class="mr-widget-body">
<mr-widget-author-and-time
actionText="Closed by"
:author="mr.closedBy"
:dateTitle="mr.updatedAt"
:dateReadable="mr.closedAt"
/>
<section>
<p>The changes were not merged into
<a :href="mr.targetBranchPath" class="label-branch">
{{mr.targetBranch}}
</a>.
</p>
</section>
</div>
`,
};
export default {
name: 'MRWidgetConflicts',
props: {
mr: { type: Object, required: true },
},
computed: {
showResolveConflictsButton() {
const { canMerge, canResolveConflicts, canResolveConflictsInUI } = this.mr;
return canMerge && canResolveConflicts && canResolveConflictsInUI;
},
},
template: `
<div class="mr-widget-body">
<button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
<span class="bold">
There are merge conflicts.
<span v-if="!mr.canMerge">Resolve these conflicts or ask someone with write access to this repository to merge it locally.</span>
</span>
<div class="btn-group">
<a
:href="mr.conflictResolutionPath"
v-if="showResolveConflictsButton"
class="btn btn-default btn-xs js-resolve-conflicts-button"
>Resolve conflicts</a>
<a
v-if="mr.canMerge"
class="btn btn-default btn-xs js-merge-locally-button"
data-toggle="modal"
href="#modal_merge_info"
>Merge locally</a>
</div>
</div>
`,
};
export default {
name: 'MRWidgetLocked',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body">
<span class="bold">Locked</span> This merge request is in the process of being merged, during which time it is locked and cannot be closed.
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<section>
<p>The changes will be merged into
<a :href="mr.targetBranchPath" class="label-branch">
{{mr.targetBranch}}
</a>
</p>
</section>
</div>
`,
};
import MRWidgetAuthor from '../../components/mr_widget_author';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const { shouldRemoveSourceBranch, canRemoveSourceBranch,
mergeUserId, currentUserId } = this.mr;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
},
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.json())
.then((res) => {
this.mr.setData(res); // TODO: Should find a better way to update store.
});
// TODO: Handle catch here.
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options); // TODO: Response and error handling, widget update
},
},
template: `
<div class="mr-widget-body">
<h4>
Set by
<mr-widget-author :author="mr.setToMWPSBy" />
to be merged automatically when the pipeline succeeds.
<button
v-if="mr.canCancelAutomaticMerge"
@click="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
type="button" class="btn btn-xs btn-default">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin" aria-hidden="true"></i>
Cancel automatic merge</button>
</h4>
<section>
<p>The changes will be merged into
<a :href="mr.targetBranchPath" class="label-branch">
{{mr.targetBranch}}
</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">The source branch will be removed.</p>
<p v-else>
The source branch will not be removed.
<button
v-if="canRemoveSourceBranch"
@click="removeSourceBranch"
type="button" class="btn btn-xs btn-default">
<i
v-if="isRemovingSourceBranch"
class="fa fa-spinner fa-spin" aria-hidden="true"></i>
Remove source branch</button>
</p>
</section>
</div>
`,
};
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
export default {
name: 'MRWidgetMerged',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
},
data() {
return {
isRemovingSourceBranch: false,
};
},
methods: {
removeSourceBranch() {
this.isRemovingSourceBranch = true;
this.service.removeSourceBranch()
.then(res => res.json()); // TODO: Update widget, handle error
},
},
template: `
<div class="mr-widget-body">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.mergedBy"
:dateTitle="mr.updatedAt"
:dateReadable="mr.mergedAt"
/>
<section class="mr-info-list">
<div class="legend"></div>
<p>
The changes were merged into
<a :href="mr.targetBranchPath" class="label-branch">
{{mr.targetBranch}}
</a>
</p>
<p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
<p v-if="mr.canRemoveSourceBranch">
You can remove source branch now.
<button
@click="removeSourceBranch"
:class="{ disabled: isRemovingSourceBranch }"
type="button" class="btn btn-xs btn-default">Remove Source Branch</button>
</p>
<p v-if="isRemovingSourceBranch">
The source branch is being removed.
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</p>
</section>
<div class="merged-buttons clearfix">
<a
v-if="mr.canRevert"
class="btn btn-close btn-sm has-tooltip"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
data-original-title="Revert this merge request in a new merge request">Revert</a>
<a
v-if="mr.canBeCherryPicked"
class="btn btn-default btn-sm has-tooltip"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
data-original-title="Cherry-pick this merge request in a new merge request">Cherry-pick</a>
</div>
</div>
`,
};
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default {
name: 'MRWidgetMissingBranch',
props: {
mr: { type: Object, required: true },
},
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
},
template: `
<div class="mr-widget-body">
<button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
<span class="bold">
<span class="capitalize">{{missingBranchName}}</span> branch does not exist.
Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
</span>
<mr-widget-merge-help :missing-branch="missingBranchName" />
</div>
`,
};
export default {
name: 'MRWidgetNotAllowed',
template: `
<div class="mr-widget-body">
<button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
<span class="bold">
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request.
</span>
</div>
`,
};
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