Commit 23457cfb authored by Phil Hughes's avatar Phil Hughes Committed by Igor Drozdov

Move approvals components to FOSS

This moves some approvals rules into FOSS
which will be a simpler version of our approval rules
and pnly require a single approve button to be shown

https://gitlab.com/gitlab-org/gitlab/-/issues/27426
parent 08963142
<script>
import { GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
import MrWidgetIcon from '../mr_widget_icon.vue';
import ApprovalsSummary from './approvals_summary.vue';
import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages';
export default {
name: 'MRWidgetApprovals',
components: {
MrWidgetContainer,
MrWidgetIcon,
ApprovalsSummary,
ApprovalsSummaryOptional,
GlButton,
},
mixins: [approvalsMixin],
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
isOptionalDefault: {
type: Boolean,
required: false,
default: null,
},
approveDefault: {
type: Function,
required: false,
default: null,
},
modalId: {
type: String,
required: false,
default: null,
},
requirePasswordToApprove: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
fetchingApprovals: true,
hasApprovalAuthError: false,
isApproving: false,
};
},
computed: {
isBasic() {
return this.mr.approvalsWidgetType === 'base';
},
isApproved() {
return Boolean(this.approvals.approved);
},
isOptional() {
return this.isOptionalDefault !== null ? this.isOptionalDefault : !this.approvedBy.length;
},
hasAction() {
return Boolean(this.action);
},
approvals() {
return this.mr.approvals || {};
},
approvedBy() {
return this.approvals.approved_by ? this.approvals.approved_by.map(x => x.user) : [];
},
userHasApproved() {
return Boolean(this.approvals.user_has_approved);
},
userCanApprove() {
return Boolean(this.approvals.user_can_approve);
},
showApprove() {
return !this.userHasApproved && this.userCanApprove && this.mr.isOpen;
},
showUnapprove() {
return this.userHasApproved && !this.userCanApprove && this.mr.state !== 'merged';
},
approvalText() {
return this.isApproved && this.approvedBy.length > 0
? s__('mrWidget|Approve additionally')
: s__('mrWidget|Approve');
},
action() {
// Use the default approve action, only if we aren't using the auth component for it
if (this.showApprove) {
return {
text: this.approvalText,
category: this.isApproved ? 'secondary' : 'primary',
variant: 'info',
action: () => this.approve(),
};
} else if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
variant: 'warning',
category: 'secondary',
action: () => this.unapprove(),
};
}
return null;
},
},
created() {
this.refreshApprovals()
.then(() => {
this.fetchingApprovals = false;
})
.catch(() => createFlash(FETCH_ERROR));
},
methods: {
approve() {
if (this.requirePasswordToApprove) {
this.$root.$emit('bv::show::modal', this.modalId);
return;
}
this.updateApproval(
() => this.service.approveMergeRequest(),
() => createFlash(APPROVE_ERROR),
);
},
approveWithAuth(data) {
this.updateApproval(
() => this.service.approveMergeRequestWithAuth(data),
error => {
if (error && error.response && error.response.status === 401) {
this.hasApprovalAuthError = true;
return;
}
createFlash(APPROVE_ERROR);
},
);
},
unapprove() {
this.updateApproval(
() => this.service.unapproveMergeRequest(),
() => createFlash(UNAPPROVE_ERROR),
);
},
updateApproval(serviceFn, errFn) {
this.isApproving = true;
this.clearError();
return serviceFn()
.then(data => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
this.$emit('updated');
})
.catch(errFn)
.then(() => {
this.isApproving = false;
});
},
},
FETCH_LOADING,
};
</script>
<template>
<mr-widget-container>
<div class="js-mr-approvals d-flex align-items-start align-items-md-center">
<mr-widget-icon name="approval" />
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
<template v-else>
<gl-button
v-if="action"
:variant="action.variant"
:category="action.category"
:loading="isApproving"
class="mr-3"
data-qa-selector="approve_button"
@click="action.action"
>
{{ action.text }}
</gl-button>
<approvals-summary-optional
v-if="isOptional"
:can-approve="hasAction"
:help-path="mr.approvalsHelpPath"
/>
<approvals-summary
v-else
:approved="isApproved"
:approvals-left="approvals.approvals_left || 0"
:rules-left="approvals.approvalRuleNamesLeft"
:approvers="approvedBy"
/>
<slot
:is-approving="isApproving"
:approve-with-auth="approveWithAuth"
:hasApproval-auth-error="hasApprovalAuthError"
></slot>
</template>
</div>
<template #footer>
<slot name="footer"></slot>
</template>
</mr-widget-container>
</template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { n__, sprintf } from '~/locale'; import { n__, sprintf } from '~/locale';
import { toNounSeriesText } from '~/lib/utils/grammar'; import { toNounSeriesText } from '~/lib/utils/grammar';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import { APPROVED_MESSAGE } from './messages'; import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
export default { export default {
components: { components: {
......
<script> <script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { OPTIONAL, OPTIONAL_CAN_APPROVE } from './messages'; import {
OPTIONAL,
OPTIONAL_CAN_APPROVE,
} from '~/vue_merge_request_widget/components/approvals/messages';
export default { export default {
components: { components: {
......
import { hideFlash } from '~/flash';
export default {
methods: {
clearError() {
this.$emit('clearError');
this.hasApprovalAuthError = false;
const flashEl = document.querySelector('.flash-alert');
if (flashEl) {
hideFlash(flashEl);
}
},
refreshApprovals() {
return this.service.fetchApprovals().then(data => {
this.mr.setApprovals(data);
});
},
},
};
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { sprintf, s__, __ } from '~/locale'; import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project'; import Project from '~/pages/projects/project';
...@@ -80,6 +81,7 @@ export default { ...@@ -80,6 +81,7 @@ export default {
GroupedTestReportsApp, GroupedTestReportsApp,
TerraformPlan, TerraformPlan,
GroupedAccessibilityReportsApp, GroupedAccessibilityReportsApp,
MrWidgetApprovals,
}, },
props: { props: {
mrData: { mrData: {
...@@ -98,6 +100,9 @@ export default { ...@@ -98,6 +100,9 @@ export default {
}; };
}, },
computed: { computed: {
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
componentName() { componentName() {
return stateMaps.stateToComponentMap[this.mr.state]; return stateMaps.stateToComponentMap[this.mr.state];
}, },
...@@ -221,6 +226,9 @@ export default { ...@@ -221,6 +226,9 @@ export default {
mergeRequestCachedWidgetPath: store.mergeRequestCachedWidgetPath, mergeRequestCachedWidgetPath: store.mergeRequestCachedWidgetPath,
mergeActionsContentPath: store.mergeActionsContentPath, mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath, rebasePath: store.rebasePath,
apiApprovalsPath: store.apiApprovalsPath,
apiApprovePath: store.apiApprovePath,
apiUnapprovePath: store.apiUnapprovePath,
}; };
}, },
createService(store) { createService(store) {
...@@ -384,6 +392,12 @@ export default { ...@@ -384,6 +392,12 @@ export default {
class="mr-widget-workflow" class="mr-widget-workflow"
:mr="mr" :mr="mr"
/> />
<mr-widget-approvals
v-if="shouldRenderApprovals"
class="mr-widget-workflow"
:mr="mr"
:service="service"
/>
<div class="mr-section-container mr-widget-workflow"> <div class="mr-section-container mr-widget-workflow">
<grouped-codequality-reports-app <grouped-codequality-reports-app
v-if="shouldRenderCodeQuality" v-if="shouldRenderCodeQuality"
......
...@@ -3,6 +3,10 @@ import axios from '../../lib/utils/axios_utils'; ...@@ -3,6 +3,10 @@ import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService { export default class MRWidgetService {
constructor(endpoints) { constructor(endpoints) {
this.endpoints = endpoints; this.endpoints = endpoints;
this.apiApprovalsPath = endpoints.apiApprovalsPath;
this.apiApprovePath = endpoints.apiApprovePath;
this.apiUnapprovePath = endpoints.apiUnapprovePath;
} }
merge(data) { merge(data) {
...@@ -54,6 +58,18 @@ export default class MRWidgetService { ...@@ -54,6 +58,18 @@ export default class MRWidgetService {
return axios.post(this.endpoints.rebasePath); return axios.post(this.endpoints.rebasePath);
} }
fetchApprovals() {
return axios.get(this.apiApprovalsPath).then(res => res.data);
}
approveMergeRequest() {
return axios.post(this.apiApprovePath).then(res => res.data);
}
unapproveMergeRequest() {
return axios.post(this.apiUnapprovePath).then(res => res.data);
}
static executeInlineAction(url) { static executeInlineAction(url) {
return axios.post(url); return axios.post(url);
} }
......
...@@ -9,12 +9,19 @@ export default class MergeRequestStore { ...@@ -9,12 +9,19 @@ export default class MergeRequestStore {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo; this.gitlabLogo = data.gitlabLogo;
this.apiApprovalsPath = data.api_approvals_path;
this.apiApprovePath = data.api_approve_path;
this.apiUnapprovePath = data.api_unapprove_path;
this.hasApprovalsAvailable = data.has_approvals_available;
this.setPaths(data); this.setPaths(data);
this.setData(data); this.setData(data);
} }
setData(data, isRebased) { setData(data, isRebased) {
this.initApprovals();
if (isRebased) { if (isRebased) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
} }
...@@ -52,6 +59,7 @@ export default class MergeRequestStore { ...@@ -52,6 +59,7 @@ export default class MergeRequestStore {
this.squashCommitMessage = data.default_squash_commit_message; this.squashCommitMessage = data.default_squash_commit_message;
this.rebaseInProgress = data.rebase_in_progress; this.rebaseInProgress = data.rebase_in_progress;
this.mergeRequestDiffsPath = data.diffs_path; this.mergeRequestDiffsPath = data.diffs_path;
this.approvalsWidgetType = data.approvals_widget_type;
if (data.issues_links) { if (data.issues_links) {
const links = data.issues_links; const links = data.issues_links;
...@@ -181,6 +189,7 @@ export default class MergeRequestStore { ...@@ -181,6 +189,7 @@ export default class MergeRequestStore {
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path; this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path; this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path; this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path;
this.approvalsHelpPath = data.approvals_help_path;
this.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path; this.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path;
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path; this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access; this.humanAccess = data.human_access;
...@@ -251,4 +260,14 @@ export default class MergeRequestStore { ...@@ -251,4 +260,14 @@ export default class MergeRequestStore {
return undefined; return undefined;
} }
initApprovals() {
this.isApproved = this.isApproved || false;
this.approvals = this.approvals || null;
}
setApprovals(data) {
this.approvals = data;
this.isApproved = data.approved || false;
}
} }
...@@ -149,7 +149,10 @@ module IssuableCollections ...@@ -149,7 +149,10 @@ module IssuableCollections
when 'Issue' when 'Issue'
common_attributes + [:project, project: :namespace] common_attributes + [:project, project: :namespace]
when 'MergeRequest' when 'MergeRequest'
common_attributes + [:target_project, :latest_merge_request_diff, source_project: :route, head_pipeline: :project, target_project: :namespace] common_attributes + [
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
source_project: :route, head_pipeline: :project, target_project: :namespace
]
end end
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
......
- merge_request = local_assigns.fetch(:merge_request)
- self_approved = merge_request.approved_by?(current_user)
- total = merge_request.approvals.size
- if total > 0
- final_text = n_("%d approver", "%d approvers", total) % total
- final_self_text = n_("%d approver (you've approved)", "%d approvers (you've approved)", total) % total
- approval_icon = sprite_icon((self_approved ? 'approval-solid' : 'approval'), size: 16, css_class: 'align-middle')
%li.d-none.d-sm-inline-block.has-tooltip.text-success{ title: self_approved ? final_self_text : final_text }
= approval_icon
= _("Approved")
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
- if merge_request.assignees.any? - if merge_request.assignees.any?
%li.d-flex %li.d-flex
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request = render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
= render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request = render 'projects/merge_requests/approvals_count', merge_request: merge_request
= render 'shared/issuable_meta_data', issuable: merge_request = render 'shared/issuable_meta_data', issuable: merge_request
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}'; window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}'; window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}'; window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}'; window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}'; window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
......
---
title: Show Approve button on merge requests in Core
merge_request: 36449
author:
type: added
<script> <script>
import { GlButton } from '@gitlab/ui'; import createFlash from '~/flash';
import createFlash, { hideFlash } from '~/flash'; import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import { s__ } from '~/locale'; import approvalsMixin from '~/vue_merge_request_widget/mixins/approvals';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
import ApprovalsSummary from './approvals_summary.vue';
import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
import ApprovalsFooter from './approvals_footer.vue';
import ApprovalsAuth from './approvals_auth.vue'; import ApprovalsAuth from './approvals_auth.vue';
import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages'; import { FETCH_ERROR } from '~/vue_merge_request_widget/components/approvals/messages';
import ApprovalsFooter from './approvals_footer.vue';
export default { export default {
name: 'MRWidgetMultipleRuleApprovals', name: 'MRWidgetMultipleRuleApprovals',
components: { components: {
MrWidgetContainer, Approvals,
MrWidgetIcon,
ApprovalsSummary,
ApprovalsSummaryOptional,
ApprovalsFooter,
ApprovalsAuth, ApprovalsAuth,
GlButton, ApprovalsFooter,
}, },
mixins: [approvalsMixin],
props: { props: {
mr: { mr: {
type: Object, type: Object,
...@@ -34,75 +26,32 @@ export default { ...@@ -34,75 +26,32 @@ export default {
}, },
data() { data() {
return { return {
fetchingApprovals: true,
isApproving: false,
isExpanded: false,
isLoadingRules: false, isLoadingRules: false,
hasApprovalAuthError: false, isExpanded: false,
modalId: 'approvals-auth', modalId: 'approvals-auth',
}; };
}, },
computed: { computed: {
isBasic() {
return this.mr.approvalsWidgetType === 'base';
},
approvals() { approvals() {
return this.mr.approvals || {}; return this.mr.approvals || {};
}, },
hasFooter() {
return Boolean(this.mr.approvals);
},
approvedBy() { approvedBy() {
return this.approvals.approved_by ? this.approvals.approved_by.map(x => x.user) : []; return this.approvals.approved_by ? this.approvals.approved_by.map(x => x.user) : [];
}, },
isApproved() {
return Boolean(this.approvals.approved);
},
approvalsRequired() { approvalsRequired() {
return this.approvals.approvals_required || 0; return (!this.isBasic && this.approvals.approvals_required) || 0;
}, },
isOptional() { isOptional() {
return !this.approvedBy.length && !this.approvalsRequired; return !this.approvedBy.length && !this.approvalsRequired;
}, },
userHasApproved() { hasFooter() {
return Boolean(this.approvals.user_has_approved); return Boolean(this.mr.approvals);
},
userCanApprove() {
return Boolean(this.approvals.user_can_approve);
},
showApprove() {
return !this.userHasApproved && this.userCanApprove && this.mr.isOpen;
},
showUnapprove() {
return this.userHasApproved && !this.userCanApprove && this.mr.state !== 'merged';
}, },
requirePasswordToApprove() { requirePasswordToApprove() {
return this.mr.approvals.require_password_to_approve; return !this.isBasic && this.approvals.require_password_to_approve;
},
approvalText() {
return this.isApproved && this.approvedBy.length > 0
? s__('mrWidget|Approve additionally')
: s__('mrWidget|Approve');
},
action() {
// Use the default approve action, only if we aren't using the auth component for it
if (this.showApprove) {
return {
text: this.approvalText,
category: this.isApproved ? 'secondary' : 'primary',
variant: 'info',
action: () => this.approve(),
};
} else if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
variant: 'warning',
category: 'secondary',
action: () => this.unapprove(),
};
}
return null;
},
hasAction() {
return Boolean(this.action);
}, },
}, },
watch: { watch: {
...@@ -112,27 +61,19 @@ export default { ...@@ -112,27 +61,19 @@ export default {
} }
}, },
}, },
created() {
this.refreshApprovals()
.then(() => {
this.fetchingApprovals = false;
})
.catch(() => createFlash(FETCH_ERROR));
},
methods: { methods: {
clearError() {
this.hasApprovalAuthError = false;
const flashEl = document.querySelector('.flash-alert');
if (flashEl) {
hideFlash(flashEl);
}
},
refreshAll() { refreshAll() {
if (this.isBasic) return Promise.resolve();
return Promise.all([this.refreshRules(), this.refreshApprovals()]).catch(() => return Promise.all([this.refreshRules(), this.refreshApprovals()]).catch(() =>
createFlash(FETCH_ERROR), createFlash(FETCH_ERROR),
); );
}, },
refreshRules() { refreshRules() {
if (this.isBasic) return Promise.resolve();
this.$root.$emit('bv::hide::modal', this.modalId);
this.isLoadingRules = true; this.isLoadingRules = true;
return this.service.fetchApprovalSettings().then(settings => { return this.service.fetchApprovalSettings().then(settings => {
...@@ -140,105 +81,37 @@ export default { ...@@ -140,105 +81,37 @@ export default {
this.isLoadingRules = false; this.isLoadingRules = false;
}); });
}, },
refreshApprovals() {
return this.service.fetchApprovals().then(data => {
this.mr.setApprovals(data);
});
},
approve() {
if (this.requirePasswordToApprove) {
this.$root.$emit('bv::show::modal', this.modalId);
return;
}
this.updateApproval(
() => this.service.approveMergeRequest(),
() => createFlash(APPROVE_ERROR),
);
},
unapprove() {
this.updateApproval(
() => this.service.unapproveMergeRequest(),
() => createFlash(UNAPPROVE_ERROR),
);
},
approveWithAuth(data) {
this.updateApproval(
() => this.service.approveMergeRequestWithAuth(data),
error => {
if (error && error.response && error.response.status === 401) {
this.hasApprovalAuthError = true;
return;
}
createFlash(APPROVE_ERROR);
},
);
},
updateApproval(serviceFn, errFn) {
this.isApproving = true;
this.clearError();
return serviceFn()
.then(data => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
this.$root.$emit('bv::hide::modal', this.modalId);
})
.catch(errFn)
.then(() => {
this.isApproving = false;
this.refreshRules();
});
},
}, },
FETCH_LOADING,
}; };
</script> </script>
<template> <template>
<mr-widget-container> <approvals
<div class="js-mr-approvals d-flex align-items-start align-items-md-center"> :mr="mr"
<mr-widget-icon name="approval" /> :service="service"
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div> :is-optional-default="isOptional"
<template v-else> :require-password-to-approve="requirePasswordToApprove"
<approvals-auth :modal-id="modalId"
:is-approving="isApproving" @updated="refreshRules"
:has-error="hasApprovalAuthError" >
:modal-id="modalId" <template v-if="!isBasic" #default="{ isApproving, approveWithAuth, hasApprovalAuthError }">
@approve="approveWithAuth" <approvals-auth
@hide="clearError" :is-approving="isApproving"
/> :has-error="hasApprovalAuthError"
<gl-button :modal-id="modalId"
v-if="action" @approve="approveWithAuth"
:variant="action.variant" @hide="clearError"
:category="action.category" />
:loading="isApproving" </template>
class="mr-3" <template v-if="!isBasic" #footer>
data-qa-selector="approve_button" <approvals-footer
@click="action.action" v-if="hasFooter"
> v-model="isExpanded"
{{ action.text }} :suggested-approvers="approvals.suggested_approvers"
</gl-button> :approval-rules="mr.approvalRules"
<approvals-summary-optional :is-loading-rules="isLoadingRules"
v-if="isOptional" :security-approvals-help-page-path="mr.securityApprovalsHelpPagePath"
:can-approve="hasAction" :eligible-approvers-docs-path="mr.eligibleApproversDocsPath"
:help-path="mr.approvalsHelpPath" />
/> </template>
<approvals-summary </approvals>
v-else
:approved="isApproved"
:approvals-left="approvals.approvals_left"
:rules-left="approvals.approvalRuleNamesLeft"
:approvers="approvedBy"
/>
</template>
</div>
<approvals-footer
v-if="hasFooter"
slot="footer"
v-model="isExpanded"
:suggested-approvers="approvals.suggested_approvers"
:approval-rules="mr.approvalRules"
:is-loading-rules="isLoadingRules"
:security-approvals-help-page-path="mr.securityApprovalsHelpPagePath"
:eligible-approvers-docs-path="mr.eligibleApproversDocsPath"
/>
</mr-widget-container>
</template> </template>
...@@ -10,7 +10,6 @@ import BlockingMergeRequestsReport from './components/blocking_merge_requests/bl ...@@ -10,7 +10,6 @@ import BlockingMergeRequestsReport from './components/blocking_merge_requests/bl
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; 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 MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violation.vue'; import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violation.vue';
import MergeTrainHelperText from './components/merge_train_helper_text.vue'; import MergeTrainHelperText from './components/merge_train_helper_text.vue';
...@@ -20,7 +19,6 @@ export default { ...@@ -20,7 +19,6 @@ export default {
components: { components: {
MergeTrainHelperText, MergeTrainHelperText,
MrWidgetLicenses, MrWidgetLicenses,
MrWidgetApprovals,
MrWidgetGeoSecondaryNode, MrWidgetGeoSecondaryNode,
MrWidgetPolicyViolation, MrWidgetPolicyViolation,
BlockingMergeRequestsReport, BlockingMergeRequestsReport,
...@@ -41,9 +39,6 @@ export default { ...@@ -41,9 +39,6 @@ export default {
}; };
}, },
computed: { computed: {
shouldRenderApprovals() {
return this.mr.hasApprovalsAvailable && this.mr.state !== 'nothingToMerge';
},
shouldRenderLicenseReport() { shouldRenderLicenseReport() {
return this.mr.enabledReports?.licenseScanning; return this.mr.enabledReports?.licenseScanning;
}, },
...@@ -193,10 +188,7 @@ export default { ...@@ -193,10 +188,7 @@ export default {
return { return {
...base, ...base,
apiApprovalsPath: store.apiApprovalsPath,
apiApprovalSettingsPath: store.apiApprovalSettingsPath, apiApprovalSettingsPath: store.apiApprovalSettingsPath,
apiApprovePath: store.apiApprovePath,
apiUnapprovePath: store.apiUnapprovePath,
}; };
}, },
......
...@@ -5,31 +5,17 @@ export default class MRWidgetService extends CEWidgetService { ...@@ -5,31 +5,17 @@ export default class MRWidgetService extends CEWidgetService {
constructor(mr) { constructor(mr) {
super(mr); super(mr);
this.apiApprovalsPath = mr.apiApprovalsPath;
this.apiApprovalSettingsPath = mr.apiApprovalSettingsPath; this.apiApprovalSettingsPath = mr.apiApprovalSettingsPath;
this.apiApprovePath = mr.apiApprovePath;
this.apiUnapprovePath = mr.apiUnapprovePath;
} }
fetchApprovals() {
return axios.get(this.apiApprovalsPath).then(res => res.data);
}
fetchApprovalSettings() {
return axios.get(this.apiApprovalSettingsPath).then(res => res.data);
}
approveMergeRequest() {
return axios.post(this.apiApprovePath).then(res => res.data);
}
approveMergeRequestWithAuth(approvalPassword) { approveMergeRequestWithAuth(approvalPassword) {
return axios return axios
.post(this.apiApprovePath, { approval_password: approvalPassword }) .post(this.apiApprovePath, { approval_password: approvalPassword })
.then(res => res.data); .then(res => res.data);
} }
unapproveMergeRequest() { fetchApprovalSettings() {
return axios.post(this.apiUnapprovePath).then(res => res.data); return axios.get(this.apiApprovalSettingsPath).then(res => res.data);
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
......
...@@ -15,7 +15,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -15,7 +15,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path; this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path;
this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback; this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path; this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
this.approvalsHelpPath = data.approvals_help_path;
this.securityReportsPipelineId = data.pipeline_id; this.securityReportsPipelineId = data.pipeline_id;
this.securityReportsPipelineIid = data.pipeline_iid; this.securityReportsPipelineIid = data.pipeline_iid;
this.createVulnerabilityFeedbackIssuePath = data.create_vulnerability_feedback_issue_path; this.createVulnerabilityFeedbackIssuePath = data.create_vulnerability_feedback_issue_path;
...@@ -35,16 +34,11 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -35,16 +34,11 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.blockingMergeRequests = data.blocking_merge_requests; this.blockingMergeRequests = data.blocking_merge_requests;
this.hasApprovalsAvailable = data.has_approvals_available;
this.apiApprovalsPath = data.api_approvals_path;
this.apiApprovalSettingsPath = data.api_approval_settings_path; this.apiApprovalSettingsPath = data.api_approval_settings_path;
this.apiApprovePath = data.api_approve_path;
this.apiUnapprovePath = data.api_unapprove_path;
} }
setData(data, isRebased) { setData(data, isRebased) {
this.initGeo(data); this.initGeo(data);
this.initApprovals();
this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled); this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
this.mergeTrainsCount = data.merge_trains_count || 0; this.mergeTrainsCount = data.merge_trains_count || 0;
...@@ -59,15 +53,16 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -59,15 +53,16 @@ export default class MergeRequestStore extends CEMergeRequestStore {
} }
initApprovals() { initApprovals() {
this.isApproved = this.isApproved || false; super.initApprovals();
this.approvals = this.approvals || null;
this.approvalRules = this.approvalRules || []; this.approvalRules = this.approvalRules || [];
} }
setApprovals(data) { setApprovals(data) {
super.setApprovals(data);
this.approvals = mapApprovalsResponse(data); this.approvals = mapApprovalsResponse(data);
this.approvalsLeft = Boolean(data.approvals_left); this.approvalsLeft = Boolean(data.approvals_left);
this.isApproved = data.approved || false;
this.preventMerge = !this.isApproved; this.preventMerge = !this.isApproved;
} }
......
...@@ -9,7 +9,7 @@ module EE ...@@ -9,7 +9,7 @@ module EE
def preload_for_collection def preload_for_collection
@preload_for_collection ||= case collection_type @preload_for_collection ||= case collection_type
when 'MergeRequest' when 'MergeRequest'
super.push(:approvals, :approval_rules) super.push(:approval_rules)
when 'Issue' when 'Issue'
super.push(*issue_preloads) super.push(*issue_preloads)
else else
......
...@@ -25,3 +25,5 @@ ...@@ -25,3 +25,5 @@
= _("Approved") = _("Approved")
- else - else
= _("%{remaining_approvals} left") % { remaining_approvals: merge_request.approvals_left } = _("%{remaining_approvals} left") % { remaining_approvals: merge_request.approvals_left }
- else
= render_ce "projects/merge_requests/approvals_count", merge_request: merge_request
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}'; window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}';
window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}'; window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}';
window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}'; window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}' window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}' window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}'
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue';
import Approvals from 'ee/vue_merge_request_widget/components/approvals/approvals.vue'; import Approvals from 'ee/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from 'ee/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import ApprovalsFoss from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummaryOptional from 'ee/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
import ApprovalsFooter from 'ee/vue_merge_request_widget/components/approvals/approvals_footer.vue'; import ApprovalsFooter from 'ee/vue_merge_request_widget/components/approvals/approvals_footer.vue';
import ApprovalsAuth from 'ee/vue_merge_request_widget/components/approvals/approvals_auth.vue'; import ApprovalsAuth from 'ee/vue_merge_request_widget/components/approvals/approvals_auth.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -12,7 +14,7 @@ import { ...@@ -12,7 +14,7 @@ import {
FETCH_ERROR, FETCH_ERROR,
APPROVE_ERROR, APPROVE_ERROR,
UNAPPROVE_ERROR, UNAPPROVE_ERROR,
} from 'ee/vue_merge_request_widget/components/approvals/messages'; } from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
const TEST_HELP_PATH = 'help/path'; const TEST_HELP_PATH = 'help/path';
...@@ -44,6 +46,10 @@ describe('EE MRWidget approvals', () => { ...@@ -44,6 +46,10 @@ describe('EE MRWidget approvals', () => {
service, service,
...props, ...props,
}, },
stubs: {
approvals: ApprovalsFoss,
MrWidgetContainer,
},
}); });
}; };
...@@ -84,6 +90,7 @@ describe('EE MRWidget approvals', () => { ...@@ -84,6 +90,7 @@ describe('EE MRWidget approvals', () => {
approvalRules: [], approvalRules: [],
isOpen: true, isOpen: true,
state: 'open', state: 'open',
approvalsWidgetType: 'full',
}; };
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
...@@ -101,7 +108,7 @@ describe('EE MRWidget approvals', () => { ...@@ -101,7 +108,7 @@ describe('EE MRWidget approvals', () => {
}); });
it('shows loading message', () => { it('shows loading message', () => {
wrapper.setData({ fetchingApprovals: true }); wrapper.find(ApprovalsFoss).setData({ fetchingApprovals: true });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.text()).toContain(FETCH_LOADING); expect(wrapper.text()).toContain(FETCH_LOADING);
...@@ -332,7 +339,7 @@ describe('EE MRWidget approvals', () => { ...@@ -332,7 +339,7 @@ describe('EE MRWidget approvals', () => {
}); });
it('sets isApproving', () => { it('sets isApproving', () => {
wrapper.setData({ isApproving: true }); wrapper.find(ApprovalsFoss).setData({ isApproving: true });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findApprovalsAuth().props('isApproving')).toBe(true); expect(findApprovalsAuth().props('isApproving')).toBe(true);
...@@ -340,7 +347,7 @@ describe('EE MRWidget approvals', () => { ...@@ -340,7 +347,7 @@ describe('EE MRWidget approvals', () => {
}); });
it('sets hasError when auth fails', () => { it('sets hasError when auth fails', () => {
wrapper.setData({ hasApprovalAuthError: true }); wrapper.find(ApprovalsFoss).setData({ hasApprovalAuthError: true });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findApprovalsAuth().props('hasError')).toBe(true); expect(findApprovalsAuth().props('hasError')).toBe(true);
......
...@@ -852,18 +852,6 @@ describe('ee merge request widget options', () => { ...@@ -852,18 +852,6 @@ describe('ee merge request widget options', () => {
describe('computed', () => { describe('computed', () => {
describe('shouldRenderApprovals', () => { describe('shouldRenderApprovals', () => {
it('should return false when no approvals', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
has_approvals_available: false,
},
});
vm.mr.state = 'readyToMerge';
expect(vm.shouldRenderApprovals).toBeFalsy();
});
it('should return false when in empty state', () => { it('should return false when in empty state', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
mrData: { mrData: {
......
...@@ -36,11 +36,11 @@ module QA ...@@ -36,11 +36,11 @@ module QA
element :expand_report_button element :expand_report_button
end end
view 'ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue' do view 'app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue' do
element :approve_button element :approve_button
end end
view 'ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue' do view 'app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue' do
element :approvals_summary_content element :approvals_summary_content
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge request > User approves', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
before do
project.add_developer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
it 'approves merge request' do
click_approval_button('Approve')
expect(page).to have_content('Merge request approved')
verify_approvals_count_on_index!
click_approval_button('Revoke approval')
expect(page).to have_content('No approval required; you can still approve')
end
def verify_approvals_count_on_index!
visit(project_merge_requests_path(project, state: :all))
expect(page.all('li').any? { |item| item["title"] == "1 approver (you've approved)"}).to be true
visit project_merge_request_path(project, merge_request)
end
def click_approval_button(action)
page.within('.mr-state-widget') do
click_button(action)
end
wait_for_requests
end
end
...@@ -3,12 +3,12 @@ import { GlLink } from '@gitlab/ui'; ...@@ -3,12 +3,12 @@ import { GlLink } from '@gitlab/ui';
import { import {
OPTIONAL, OPTIONAL,
OPTIONAL_CAN_APPROVE, OPTIONAL_CAN_APPROVE,
} from 'ee/vue_merge_request_widget/components/approvals/messages'; } from '~/vue_merge_request_widget/components/approvals/messages';
import ApprovalsSummaryOptional from 'ee/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue'; import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
const TEST_HELP_PATH = 'help/path'; const TEST_HELP_PATH = 'help/path';
describe('EE MRWidget approvals summary optional', () => { describe('MRWidget approvals summary optional', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { APPROVED_MESSAGE } from 'ee/vue_merge_request_widget/components/approvals/messages'; import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
import ApprovalsSummary from 'ee/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import { toNounSeriesText } from '~/lib/utils/grammar'; import { toNounSeriesText } from '~/lib/utils/grammar';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
...@@ -8,7 +8,7 @@ const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map(id => ({ ...@@ -8,7 +8,7 @@ const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map(id => ({
const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit']; const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit'];
const TEST_APPROVALS_LEFT = 3; const TEST_APPROVALS_LEFT = 3;
describe('EE MRWidget approvals summary', () => { describe('MRWidget approvals summary', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
......
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