diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2017-01-18 20:23:53 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2017-01-18 20:23:53 +0000 |
commit | e808af8c95ba1173f87546783f00b94e87b456ea (patch) | |
tree | 211b0da231ae8a0034076d2f0b842f1a76217f53 /app | |
parent | 745f1f8d1024fea32e091269cf1f65337cb12c66 (diff) | |
parent | 3faffa180df0b8e488a94591935fbbb39c4ed7ad (diff) | |
download | gitlab-ce-e808af8c95ba1173f87546783f00b94e87b456ea.tar.gz |
Merge branch 'approval-integration' into 'master'
MR Approvals Frontend-Backend Integration
Closes #1262, #894, and #1287
See merge request !954
Diffstat (limited to 'app')
32 files changed, 951 insertions, 43 deletions
diff --git a/app/assets/images/mailers/approval/icon-merge-request-gray.gif b/app/assets/images/mailers/approval/icon-merge-request-gray.gif Binary files differnew file mode 100644 index 00000000000..30cbe66980f --- /dev/null +++ b/app/assets/images/mailers/approval/icon-merge-request-gray.gif diff --git a/app/assets/images/mailers/approval/icon-x-orange-inverted.gif b/app/assets/images/mailers/approval/icon-x-orange-inverted.gif Binary files differnew file mode 100644 index 00000000000..7fbf1c41384 --- /dev/null +++ b/app/assets/images/mailers/approval/icon-x-orange-inverted.gif diff --git a/app/assets/javascripts/generic_bundles/vue_resource.js.es6 b/app/assets/javascripts/generic_bundles/vue_resource.js.es6 new file mode 100644 index 00000000000..eff1dcabfa2 --- /dev/null +++ b/app/assets/javascripts/generic_bundles/vue_resource.js.es6 @@ -0,0 +1,2 @@ +//= require vue +//= require vue-resource diff --git a/app/assets/javascripts/merge_request_widget/approvals/approvals_api.js.es6 b/app/assets/javascripts/merge_request_widget/approvals/approvals_api.js.es6 new file mode 100644 index 00000000000..4f181e02f39 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/approvals/approvals_api.js.es6 @@ -0,0 +1,36 @@ +/* global Vue, Flash */ +//= require ./approvals_store + +(() => { + class ApprovalsApi { + constructor(endpoint) { + gl.ApprovalsApi = this; + this.init(endpoint); + } + + init(mergeRequestEndpoint) { + this.baseEndpoint = `${mergeRequestEndpoint}/approvals`; + Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); + } + + fetchApprovals() { + const flashErrorMessage = 'An error occured while retrieving approval data for this merge request.'; + + return Vue.http.get(this.baseEndpoint).catch(() => new Flash(flashErrorMessage)); + } + + approveMergeRequest() { + const flashErrorMessage = 'An error occured while submitting your approval.'; + + return Vue.http.post(this.baseEndpoint).catch(() => new Flash(flashErrorMessage)); + } + + unapproveMergeRequest() { + const flashErrorMessage = 'An error occured while removing your approval.'; + + return Vue.http.delete(this.baseEndpoint).catch(() => new Flash(flashErrorMessage)); + } + } + + gl.ApprovalsApi = ApprovalsApi; +})(); diff --git a/app/assets/javascripts/merge_request_widget/approvals/approvals_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/approvals/approvals_bundle.js.es6 new file mode 100644 index 00000000000..e5173098f93 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/approvals/approvals_bundle.js.es6 @@ -0,0 +1,3 @@ +//= require ./approvals_store +//= require ./approvals_api +//= require_directory ./components diff --git a/app/assets/javascripts/merge_request_widget/approvals/approvals_store.js.es6 b/app/assets/javascripts/merge_request_widget/approvals/approvals_store.js.es6 new file mode 100644 index 00000000000..1c96109656b --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/approvals/approvals_store.js.es6 @@ -0,0 +1,65 @@ +/* global Vue */ +//= require ./approvals_api + +(() => { + let singleton; + + class MergeRequestApprovalsStore { + constructor(rootStore) { + if (!singleton) { + singleton = this; + this.init(rootStore); + } + return singleton; + } + + init(rootStore) { + this.rootStore = rootStore; + this.api = new gl.ApprovalsApi(rootStore.rootEl.dataset.endpoint); + this.state = { + fetching: false, + }; + } + + initStoreOnce() { + const state = this.state; + if (!state.fetching) { + state.fetching = true; + return this.fetch() + .then(() => { + state.fetching = false; + this.assignToRootStore('showApprovals', true); + }); + } + return Promise.resolve(); + } + + fetch() { + return this.api.fetchApprovals() + .then(res => this.assignToRootStore('approvals', res.data)) + .then(data => this.setMergeRequestAcceptanceStatus(data.approvals_left)); + } + + approve() { + return this.api.approveMergeRequest() + .then(res => this.assignToRootStore('approvals', res.data)) + .then(data => this.setMergeRequestAcceptanceStatus(data.approvals_left)); + } + + unapprove() { + return this.api.unapproveMergeRequest() + .then(res => this.assignToRootStore('approvals', res.data)) + .then(data => this.setMergeRequestAcceptanceStatus(data.approvals_left)); + } + + setMergeRequestAcceptanceStatus(approvalsLeft) { + return this.rootStore.assignToData('disableAcceptance', !!approvalsLeft); + } + + assignToRootStore(key, data) { + return this.rootStore.assignToData(key, data); + } + } + gl.MergeRequestApprovalsStore = MergeRequestApprovalsStore; +})(window.gl || (window.gl = {})); + diff --git a/app/assets/javascripts/merge_request_widget/approvals/components/approvals_body.js.es6 b/app/assets/javascripts/merge_request_widget/approvals/components/approvals_body.js.es6 new file mode 100644 index 00000000000..7451639de35 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/approvals/components/approvals_body.js.es6 @@ -0,0 +1,94 @@ +/* global Vue */ +//= require ../approvals_store +//= require ../approvals_api + +(() => { + Vue.component('approvals-body', { + name: 'approvals-body', + props: { + approvedBy: { + type: Array, + required: false, + }, + approvalsLeft: { + type: Number, + required: false, + }, + userCanApprove: { + type: Boolean, + required: false, + }, + userHasApproved: { + type: Boolean, + required: false, + }, + suggestedApprovers: { + type: Array, + required: false, + }, + }, + data() { + return { + approving: false, + }; + }, + computed: { + approvalsRequiredStringified() { + const baseString = `${this.approvalsLeft} more approval`; + return this.approvalsLeft === 1 ? baseString : `${baseString}s`; + }, + approverNamesStringified() { + const approvers = this.suggestedApprovers; + + if (!approvers) { + return ''; + } + + return approvers.length === 1 ? approvers[0].name : + approvers.reduce((memo, curr, index) => { + const nextMemo = `${memo}${curr.name}`; + + if (index === approvers.length - 2) { // second to last index + return `${nextMemo} or `; + } else if (index === approvers.length - 1) { // last index + return nextMemo; + } + + return `${nextMemo}, `; + }, ''); + }, + showApproveButton() { + return this.userCanApprove && !this.userHasApproved; + }, + showSuggestedApprovers() { + return this.suggestedApprovers && this.suggestedApprovers.length; + }, + }, + methods: { + approveMergeRequest() { + this.approving = true; + return gl.ApprovalsStore.approve().then(() => { + this.approving = false; + }); + }, + }, + beforeCreate() { + gl.ApprovalsStore.initStoreOnce(); + }, + template: ` + <div class='approvals-body mr-widget-footer'> + <h4> Requires {{ approvalsRequiredStringified }} + <span v-if='showSuggestedApprovers'> (from {{ approverNamesStringified }}) </span> + </h4> + <div v-if='showApproveButton' class='append-bottom-10'> + <button + :disabled='approving' + @click='approveMergeRequest' + class='btn btn-primary approve-btn'> + Approve Merge Request + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/merge_request_widget/approvals/components/approvals_footer.js.es6 b/app/assets/javascripts/merge_request_widget/approvals/components/approvals_footer.js.es6 new file mode 100644 index 00000000000..dfa9c62e344 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/approvals/components/approvals_footer.js.es6 @@ -0,0 +1,93 @@ +/* global Vue */ +//= require ../approvals_store +//= require vue_common_component/link_to_member_avatar + +(() => { + Vue.component('approvals-footer', { + name: 'approvals-footer', + props: { + approvedBy: { + type: Array, + required: false, + }, + approvalsLeft: { + type: Number, + required: false, + }, + userCanApprove: { + type: Boolean, + required: false, + }, + userHasApproved: { + type: Boolean, + required: false, + }, + suggestedApprovers: { + type: Array, + required: false, + }, + pendingAvatarSvg: { + type: String, + required: true, + }, + checkmarkSvg: { + type: String, + required: true, + }, + }, + data() { + return { + unapproving: false, + }; + }, + computed: { + showUnapproveButton() { + return this.userHasApproved && !this.userCanApprove; + }, + }, + methods: { + unapproveMergeRequest() { + this.unapproving = true; + gl.ApprovalsStore.unapprove().then(() => { + this.unapproving = false; + }); + }, + }, + beforeCreate() { + gl.ApprovalsStore.initStoreOnce(); + }, + template: ` + <div class='mr-widget-footer approved-by-users approvals-footer clearfix'> + <span class='approvers-prefix'> Approved by </span> + <span v-for='approver in approvedBy'> + <link-to-member-avatar + extra-link-class='approver-avatar' + :avatar-url='approver.user.avatar_url' + :display-name='approver.user.name' + :profile-url='approver.user.web_url' + :avatar-html='checkmarkSvg' + :show-tooltip='true'> + </link-to-member-avatar> + </span> + <span v-for='n in approvalsLeft'> + <link-to-member-avatar + :clickable='false' + :avatar-html='pendingAvatarSvg' + :show-tooltip='false' + extra-link-class='hide-asset'> + </link-to-member-avatar> + </span> + <span class='unapprove-btn-wrap' v-if='showUnapproveButton'> + <button + :disabled='unapproving' + @click='unapproveMergeRequest' + class='btn btn-link unapprove-btn'> + <i class='fa fa-close'></i> + Remove your approval</span> + </button> + </span> + </div> + `, + }); +})(); + diff --git a/app/assets/javascripts/merge_request_widget/widget_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/widget_bundle.js.es6 new file mode 100644 index 00000000000..0986ae242ec --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/widget_bundle.js.es6 @@ -0,0 +1,15 @@ +/* global Vue */ +//= require ./widget_store +//= require ./approvals/approvals_bundle + +(() => { + $(() => { + const rootEl = document.getElementById('merge-request-widget-app'); + const widgetSharedStore = new gl.MergeRequestWidgetStore(rootEl); + + gl.MergeRequestWidgetApp = new Vue({ + el: rootEl, + data: widgetSharedStore.data, + }); + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/widget_store.js.es6 b/app/assets/javascripts/merge_request_widget/widget_store.js.es6 new file mode 100644 index 00000000000..a934712f5b9 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/widget_store.js.es6 @@ -0,0 +1,40 @@ +//= require ./approvals/approvals_store + +(() => { + let singleton; + + class MergeRequestWidgetStore { + constructor(rootEl) { + if (!singleton) { + singleton = gl.MergeRequestWidget.Store = this; + this.init(rootEl); + } + return singleton; + } + + init(rootEl) { + this.rootEl = rootEl; + this.data = {}; + + // init other widget stores here + this.initWidgetState(); + this.initApprovals(); + } + + initWidgetState() { + this.assignToData('showApprovals', false); + this.assignToData('disableAcceptance', Boolean(this.rootEl.dataset.approvalPending)); + } + + initApprovals() { + gl.ApprovalsStore = new gl.MergeRequestApprovalsStore(this); + this.assignToData('approvals', {}); + } + + assignToData(key, val) { + this.data[key] = val; + return val; + } + } + gl.MergeRequestWidgetStore = MergeRequestWidgetStore; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_common_component/link_to_member_avatar.js.es6 b/app/assets/javascripts/vue_common_component/link_to_member_avatar.js.es6 new file mode 100644 index 00000000000..f36591c6181 --- /dev/null +++ b/app/assets/javascripts/vue_common_component/link_to_member_avatar.js.es6 @@ -0,0 +1,92 @@ +/* global Vue */ +// Analogue of link_to_member_avatar in app/helpers/projects_helper.rb + +(() => { + Vue.component('link-to-member-avatar', { + props: { + avatarUrl: { + type: String, + required: false, + default: '/assets/no_avatar.png', + }, + profileUrl: { + type: String, + required: false, + default: '', + }, + displayName: { + type: String, + required: false, + }, + extraAvatarClass: { + type: String, + default: '', + required: false, + }, + extraLinkClass: { + type: String, + default: '', + required: false, + }, + showTooltip: { + type: Boolean, + required: false, + default: true, + }, + clickable: { + type: Boolean, + default: true, + required: false, + }, + tooltipContainer: { + type: String, + required: false, + }, + avatarHtml: { + type: String, + required: false, + }, + avatarSize: { + type: Number, + required: false, + default: 32, + }, + }, + data() { + return { + avatarBaseClass: 'avatar avatar-inline', + }; + }, + computed: { + avatarSizeClass() { + return `s${this.avatarSize}`; + }, + avatarHtmlClass() { + return `${this.avatarSizeClass} ${this.avatarBaseClass}`; + }, + tooltipClass() { + return this.showTooltip ? 'has-tooltip' : ''; + }, + avatarClass() { + return `${this.avatarBaseClass} ${this.avatarSizeClass} ${this.extraAvatarClass}`; + }, + disabledClass() { + return !this.clickable ? 'disabled' : ''; + }, + linkClass() { + return `author_link ${this.tooltipClass} ${this.extraLinkClass} ${this.disabledClass}`; + }, + tooltipContainerAttr() { + return this.tooltipContainer || 'body'; + }, + }, + template: ` + <div class='link-to-member-avatar'> + <a :href='profileUrl' :class='linkClass' :data-original-title='displayName' :data-container='tooltipContainerAttr'> + <svg v-if='avatarHtml' v-html='avatarHtml' :class='avatarHtmlClass' :width='avatarSize' :height='avatarSize' :alt='displayName'></svg> + <img :class='avatarClass' :src='avatarUrl' :width='avatarSize' :height='avatarSize' :alt='displayName'/> + </a> + </div> + `, + }); +})(); diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 45ff9f7ff5f..e6b66036f94 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -440,3 +440,93 @@ padding-right: 0; } } + +#merge-request-widget-app .loading { + padding-top: 5px; +} + +#merge-request-widget-app .loading, +.approvals-components { + border-top: 1px solid $well-inner-border; +} + +.approvals-body { + @media (max-width: $screen-xs-max) { + text-align: center; + } + + .approve-btn { + margin-top: 10px; + } +} + +.approvals-footer { + display: flex; + + // vertically centers all children + > span { + align-self: center; + } + + .hide-asset { + img { + display: none; + } + + svg { + margin-bottom: -7px; // makes up for border removed + border: none; + } + } + + .approvers-prefix { + margin-right: 5px; + } + + .unapprove-btn-wrap { + border-left: 1px solid $gray-darker; + padding-left: 5px; + margin-left: 10px; + } + + .unapprove-btn { + border: none; + background: transparent; + cursor: pointer; + + &:hover { + color: $gl-text-color-secondary; + text-decoration: none; + } + + &:focus { + outline: none; + } + } + + // styles for approver avatar checkmark + .approver-avatar { + position: relative; + display: inline-block; + + svg.avatar { + position: absolute; + top: 12%; + right: 4%; + height: 45%; + width: 45%; + } + } +} + +.link-to-member-avatar { + .disabled { + pointer-events: none; + cursor: default; + } + + .avatar { + margin-bottom: -2px; + margin-right: 3px; + } +} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 44cd2466d48..0d0f81f93ce 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -11,7 +11,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, - :approve, :rebase + # EE + :approve, :approvals, :unapprove, :rebase ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] @@ -481,15 +482,38 @@ class Projects::MergeRequestsController < Projects::ApplicationController return render_404 end - MergeRequests::ApprovalService. + ::MergeRequests::ApprovalService. new(project, current_user). execute(@merge_request) - redirect_to merge_request_path(@merge_request) + render_approvals_json + end + + def approvals + render_approvals_json + end + + def unapprove + if @merge_request.has_approved?(current_user) + ::MergeRequests::RemoveApprovalService. + new(project, current_user). + execute(@merge_request) + end + + render_approvals_json end protected + def render_approvals_json + respond_to do |format| + format.json do + entity = API::Entities::MergeRequestApprovals.new(@merge_request, current_user: current_user) + render json: entity + end + end + end + def selected_target_project if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? @project diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 759d85fdb31..568dabb1e4b 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -90,6 +90,7 @@ module MergeRequestsHelper end end + # This may be able to be removed with associated specs def render_require_section(merge_request) str = if merge_request.approvals_left == 1 "Requires one more approval" diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 5bd8c37ed7b..73dea209b88 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -54,11 +54,18 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) end - def approved_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) + def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id) setup_merge_request_mail(merge_request_id, recipient_id) - @approved_by_users = @merge_request.approved_by_users.map(&:name) - mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) + @approved_by = User.find(approved_by_user_id) + mail_answer_thread(@merge_request, merge_request_thread_options(approved_by_user_id, recipient_id)) + end + + def unapproved_merge_request_email(recipient_id, merge_request_id, unapproved_by_user_id) + setup_merge_request_mail(merge_request_id, recipient_id) + + @unapproved_by = User.find(unapproved_by_user_id) + mail_answer_thread(@merge_request, merge_request_thread_options(unapproved_by_user_id, recipient_id)) end def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id) diff --git a/app/models/concerns/approvable.rb b/app/models/concerns/approvable.rb index 561a3839276..a34eac68f23 100644 --- a/app/models/concerns/approvable.rb +++ b/app/models/concerns/approvable.rb @@ -123,6 +123,12 @@ module Approvable any_approver_allowed? && approvals.where(user: user).empty? end + def has_approved?(user) + return false unless user + + approved_by_users.include?(user) + end + # Once there are fewer approvers left in the list than approvals required, allow other # project members to approve the MR. # diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb new file mode 100644 index 00000000000..8ae6651e59b --- /dev/null +++ b/app/services/merge_requests/remove_approval_service.rb @@ -0,0 +1,31 @@ +module MergeRequests + class RemoveApprovalService < MergeRequests::BaseService + def execute(merge_request) + # paranoid protection against running wrong deletes + return unless merge_request.id && current_user.id + + approval = merge_request.approvals.where(user: current_user) + + currently_approved = merge_request.approved? + + if approval.destroy_all + # bust the cache here, otherwise will show results from + # before the deletion + merge_request.approvals(true) + + create_note(merge_request) + + if currently_approved + notification_service.unapprove_mr(merge_request, current_user) + execute_hooks(merge_request, 'unapproved') + end + end + end + + private + + def create_note(merge_request) + SystemNoteService.unapprove_mr(merge_request, current_user) + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 89396156d0c..34982ff4167 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -162,6 +162,10 @@ class NotificationService approve_mr_email(merge_request, merge_request.target_project, current_user) end + def unapprove_mr(merge_request, current_user) + unapprove_mr_email(merge_request, merge_request.target_project, current_user) + end + def resolve_all_discussions(merge_request, current_user) recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions") @@ -608,6 +612,14 @@ class NotificationService end end + def unapprove_mr_email(merge_request, project, current_user) + recipients = build_recipients(merge_request, project, current_user) + + recipients.each do |recipient| + mailer.unapproved_merge_request_email(recipient.id, merge_request.id, current_user.id).deliver_later + end + end + def add_mr_approvers_email(merge_request, approvers, current_user) approvers.each do |approver| recipient = approver.user diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index c6c9d006809..9e6a3c8f31d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -474,6 +474,11 @@ module SystemNoteService create_note(noteable: noteable, project: noteable.project, author: user, note: body) end + def unapprove_mr(noteable, user) + body = "Unapproved this merge request" + create_note(noteable: noteable, project: noteable.project, author: user, note: body) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/app/views/notify/approved_merge_request_email.html.haml b/app/views/notify/approved_merge_request_email.html.haml index de2c3818494..16b9df65f5e 100644 --- a/app/views/notify/approved_merge_request_email.html.haml +++ b/app/views/notify/approved_merge_request_email.html.haml @@ -1,2 +1,146 @@ -%p - = "Merge Request #{@merge_request.to_reference} was approved by #{@approved_by_users.to_sentence}" +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + %tr.success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + %span Merge request was approved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required}) + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" } + %tbody + %tr{ style: 'width:100%;' } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" } + %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" } + %span{ style: "font-weight: bold;color:#333333;" } Merge request + %a{ href: merge_request_url(@merge_request), style: "font-weight: bold;color:#3777b0;text-decoration:none" } #{@merge_request.to_reference} + %span was approved by + %img.avatar{ height: "24", src: avatar_icon(@approved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/ + %a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" } + = @approved_by.name + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %span.muted{ style: "color:#333333;text-decoration:none;" } + = @merge_request.source_branch + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" } + = @merge_request.author.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Assignee + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(@merge_request.assignee, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" } + = @merge_request.assignee.name + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/approved_merge_request_email.text.haml b/app/views/notify/approved_merge_request_email.text.haml index 5038a70e386..1b7af953b10 100644 --- a/app/views/notify/approved_merge_request_email.text.haml +++ b/app/views/notify/approved_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was approved by #{@approved_by_users.to_sentence}" += "Merge Request #{@merge_request.to_reference} was approved by #{@approved_by_user}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/unapproved_merge_request_email.html.haml b/app/views/notify/unapproved_merge_request_email.html.haml new file mode 100644 index 00000000000..b7c5e9a89a9 --- /dev/null +++ b/app/views/notify/unapproved_merge_request_email.html.haml @@ -0,0 +1,146 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + %tr.success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✗", height: "13", src: image_url('mailers/approval/icon-x-orange-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + %span Merge request was unapproved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required}) + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" } + %tbody + %tr{ style: 'width:100%;' } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" } + %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" } + %span{ style: "font-weight: bold;color:#333333;" } Merge request + %a{ href: merge_request_url(@merge_request), style: "font-weight: bold;color:#3777b0;text-decoration:none" } #{@merge_request.to_reference} + %span was unapproved by + %img.avatar{ height: "24", src: avatar_icon(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/ + %a.muted{ href: user_url(@unapproved_by), style: "color:#333333;text-decoration:none;" } + = @unapproved_by.name + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %span.muted{ style: "color:#333333;text-decoration:none;" } + = @merge_request.source_branch + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" } + = @merge_request.author.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Assignee + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(@merge_request.assignee, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" } + = @merge_request.assignee.name + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/unapproved_merge_request_email.text.haml b/app/views/notify/unapproved_merge_request_email.text.haml new file mode 100644 index 00000000000..7e31b2f45e6 --- /dev/null +++ b/app/views/notify/unapproved_merge_request_email.text.haml @@ -0,0 +1,8 @@ += "Merge Request #{@merge_request.to_reference} was unapproved by #{@unapproved_by_user}" + +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} + += merge_path_description(@merge_request, 'to') + +Author: #{@merge_request.author_name} +Assignee: #{@merge_request.assignee_name} diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0516801cdf3..24ca7bc3fd1 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -108,8 +108,8 @@ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript - var merge_request; - - merge_request = new MergeRequest({ - action: "#{controller.action_name}" + $(function () { + new MergeRequest({ + action: "#{controller.action_name}" + }); }); diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index bfc096cfe63..a2127549a90 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -1,4 +1,9 @@ -.mr-state-widget +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/widget_bundle.js') + +- approval_pending = @merge_request.requires_approve? && !@merge_request.approved? + +#merge-request-widget-app.mr-state-widget{ 'data-endpoint'=> merge_request_path(@merge_request), 'data-approval-pending' => approval_pending } = render 'projects/merge_requests/widget/heading' .mr-widget-body -# After conflicts are resolved, the user is redirected back to the MR page. @@ -23,8 +28,6 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' - - elsif @merge_request.requires_approve? && !@merge_request.approved? - = render 'projects/merge_requests/widget/open/approve' - elsif @merge_request.merge_when_build_succeeds? = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) @@ -52,8 +55,12 @@ != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed. - - if @merge_request.approvals.any? - .mr-widget-footer.approved-by-users - Approved by - - @merge_request.approved_by_users.each do |user| - = link_to_member(@project, user, name: false, size: 24) + - if @merge_request.requires_approve? + .mr-widget-footer{ 'v-show' => '!showApprovals' } + = icon("spinner spin") + Checking approval status for this merge request. + .approvals-components{ 'v-show' => 'showApprovals' } + = render 'projects/merge_requests/widget/open/approvals_body' + = render 'projects/merge_requests/widget/open/approvals_footer' + + diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 81125aa0dac..5daeb6da6fd 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -28,7 +28,7 @@ = icon('warning fw') Merge Immediately - else - = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do + = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}", ':disabled' => 'disableAcceptance' do Accept Merge Request - if @merge_request.force_remove_source_branch? .accept-control diff --git a/app/views/projects/merge_requests/widget/open/_approvals_body.html.haml b/app/views/projects/merge_requests/widget/open/_approvals_body.html.haml new file mode 100644 index 00000000000..60d1bf166cc --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_approvals_body.html.haml @@ -0,0 +1 @@ +%approvals-body{ ':user-can-approve' => 'approvals.user_can_approve', ':user-has-approved' => 'approvals.user_has_approved', ':approved-by' => 'approvals.approved_by', ':approvals-left':'approvals.approvals_left', ':suggested-approvers' => 'approvals.suggested_approvers' } diff --git a/app/views/projects/merge_requests/widget/open/_approvals_footer.html.haml b/app/views/projects/merge_requests/widget/open/_approvals_footer.html.haml new file mode 100644 index 00000000000..87c584127cd --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_approvals_footer.html.haml @@ -0,0 +1 @@ +%approvals-footer{ 'pending-avatar-svg' => custom_icon('icon_dotted_circle'), 'checkmark-svg' => custom_icon('icon_checkmark'), ':user-can-approve' => 'approvals.user_can_approve', ':user-has-approved' => 'approvals.user_has_approved', ':approved-by' => 'approvals.approved_by', ':approvals-left':'approvals.approvals_left' } diff --git a/app/views/projects/merge_requests/widget/open/_approve.html.haml b/app/views/projects/merge_requests/widget/open/_approve.html.haml deleted file mode 100644 index c762c8597ba..00000000000 --- a/app/views/projects/merge_requests/widget/open/_approve.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%div - %h4 - = render_require_section(@merge_request) - - if @merge_request.can_approve?(current_user) - .append-bottom-10 - = form_for [:approve, @project.namespace.becomes(Namespace), @project, @merge_request], method: :post do |f| - = f.submit "Approve Merge Request", class: "btn btn-primary approve-btn" diff --git a/app/views/projects/merge_requests/widget/open/_rebase.html.haml b/app/views/projects/merge_requests/widget/open/_rebase.html.haml index d6d804cb32a..e524f05a7c8 100644 --- a/app/views/projects/merge_requests/widget/open/_rebase.html.haml +++ b/app/views/projects/merge_requests/widget/open/_rebase.html.haml @@ -1,14 +1,13 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + - if @merge_request.rebase_in_progress? || (defined?(rebase_in_progress) && rebase_in_progress) - %h4 + %h4.rebase-in-progress = icon("spinner spin") Rebase in progress… %p This merge request is in the process of being rebased. - :javascript - $(function() { - merge_request_widget.rebaseInProgress() - }); - elsif !can_push_branch?(@merge_request.source_project, @merge_request.source_branch) %h4 = icon("exclamation-triangle") @@ -26,12 +25,3 @@ Rebase onto #{@merge_request.target_branch} .accept-control Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged. - - :javascript - $('.rebase-mr-form').on('ajax:send', function() { - $('.rebase-mr-form :input').disable(); - }); - - $('.js-rebase-button').on('click', function() { - $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); - }); diff --git a/app/views/shared/icons/_icon_checkmark.svg b/app/views/shared/icons/_icon_checkmark.svg new file mode 100644 index 00000000000..d01e24d4b68 --- /dev/null +++ b/app/views/shared/icons/_icon_checkmark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m5.743 5.743h-2c-.556 0-1 .448-1 1 0 .556.448 1 1 1h3c.556 0 1-.448 1-1v-5.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v4.01"/><mask id="1" width="6" height="8" x="-.5" y="-.5"><path fill="#fff" d="m2.243.243h6v8h-6z"/><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="matrix(.70711.70711-.70711.70711 4.536-2.465)"><use fill="#31af64" xlink:href="#0"/><use stroke="#fff" mask="url(#1)" xlink:href="#0"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_dotted_circle.svg b/app/views/shared/icons/_icon_dotted_circle.svg new file mode 100644 index 00000000000..fee95e1cab5 --- /dev/null +++ b/app/views/shared/icons/_icon_dotted_circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27"><path fill="#bfbfbf" fill-rule="evenodd" d="m13.5 26.5c1.412 0 2.794-.225 4.107-.662l-.316-.949c-1.212.403-2.487.611-3.792.611v1m6.06-1.495c1.234-.651 2.355-1.498 3.321-2.504l-.721-.692c-.892.929-1.928 1.711-3.067 2.312l.467.884m4.66-4.147c.79-1.149 1.391-2.418 1.777-3.762l-.961-.276c-.356 1.24-.911 2.411-1.64 3.471l.824.567m2.184-5.761c.063-.518.096-1.041.097-1.568 0-.896-.085-1.758-.255-2.603l-.98.197c.157.78.236 1.576.236 2.405-.001.486-.031.97-.09 1.448l.993.122m-.738-6.189c-.493-1.307-1.195-2.523-2.075-3.605l-.776.631c.812.999 1.46 2.122 1.916 3.327l.935-.353m-3.539-5.133c-1.043-.926-2.229-1.68-3.512-2.229l-.394.919c1.184.507 2.279 1.203 3.242 2.058l.664-.748m-5.463-2.886c-1.012-.253-2.058-.384-3.119-.388-.378 0-.717.013-1.059.039l.077.997c.316-.024.629-.036.98-.036.979.003 1.944.124 2.879.358l.243-.97m-6.238-.022c-1.361.33-2.653.878-3.832 1.619l.532.847c1.089-.684 2.281-1.189 3.536-1.494l-.236-.972m-5.517 2.878c-1.047.922-1.94 2.01-2.643 3.212l.864.504c.649-1.112 1.474-2.114 2.441-2.966l-.661-.75m-3.54 5.076c-.499 1.293-.789 2.664-.854 4.072l.999.046c.06-1.3.328-2.564.788-3.758l-.933-.36m-.78 6.202c.163 1.396.549 2.744 1.14 4l.905-.425c-.545-1.16-.902-2.404-1.052-3.692l-.993.116m2.177 5.814c.788 1.151 1.756 2.169 2.866 3.01l.606-.796c-1.025-.78-1.919-1.721-2.646-2.783l-.825.565m4.665 4.164c1.23.65 2.559 1.1 3.943 1.328l.162-.987c-1.278-.21-2.503-.625-3.638-1.225l-.468.884m6.02 1.501c.024 0 .024 0 .048 0v-1c-.022 0-.022 0-.044 0l-.004 1"/></svg>
\ No newline at end of file |