Skip to content
Snippets Groups Projects
Verified Commit 60373d32 authored by Kushal Pandya's avatar Kushal Pandya :speech_balloon: Committed by GitLab
Browse files

Merge branch 'ph/448425/reviewerDrawerApprovalHeader' into 'master'

Added approval summary header to reviewer drawer component

See merge request !148384



Merged-by: Kushal Pandya's avatarKushal Pandya <kushal@gitlab.com>
Approved-by: Kushal Pandya's avatarKushal Pandya <kushal@gitlab.com>
Co-authored-by: default avatarPhil Hughes <me@iamphill.com>
parents 6cc7bbba 0c8002a2
No related branches found
No related tags found
1 merge request!148384Added approval summary header to reviewer drawer component
Pipeline #1244131225 passed
Showing
with 328 additions and 14 deletions
......@@ -6,6 +6,8 @@ import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
export default {
components: {
GlDrawer,
ApprovalSummary: () =>
import('ee_component/merge_requests/components/reviewers/approval_summary.vue'),
},
props: {
open: {
......@@ -33,5 +35,8 @@ export default {
<template #title>
<h4 class="gl-my-0">{{ __('Assign reviewers') }}</h4>
</template>
<template #header>
<approval-summary />
</template>
</gl-drawer>
</template>
......@@ -59,18 +59,6 @@ export default {
{{ reviewerTitle }}
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<template v-if="editable">
<gl-button
v-if="glFeatures.reviewerAssignDrawer"
v-tooltip.hover
:title="__('Add or edit reviewers')"
category="tertiary"
size="small"
class="gl-float-right gl-ml-2"
data-testid="drawer-toggle"
@click="toggleDrawerOpen"
>
{{ __('Edit') }}
</gl-button>
<gl-button
v-tooltip.hover
:title="__('Quick assign')"
......@@ -84,6 +72,18 @@ export default {
:icon="glFeatures.reviewerAssignDrawer ? 'plus' : ''"
><template v-if="!glFeatures.reviewerAssignDrawer">{{ __('Edit') }}</template></gl-button
>
<gl-button
v-if="glFeatures.reviewerAssignDrawer"
v-tooltip.hover
:title="__('Add or edit reviewers')"
category="tertiary"
size="small"
class="gl-float-right gl-ml-2"
data-testid="drawer-toggle"
@click="toggleDrawerOpen(!drawerOpen)"
>
{{ __('Edit') }}
</gl-button>
</template>
<mounting-portal v-if="glFeatures.reviewerAssignDrawer" mount-to="#js-reviewer-drawer-portal">
<reviewer-drawer :open="drawerOpen" @close="toggleDrawerOpen(false)" />
......
......@@ -187,12 +187,18 @@ function mountSidebarReviewers(mediator) {
return;
}
const { iid, fullPath } = getSidebarOptions();
const { id, iid, fullPath, multipleApprovalRulesAvailable = false } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
name: 'SidebarReviewersRoot',
apolloProvider,
provide: {
issuableIid: String(iid),
issuableId: String(id),
projectPath: fullPath,
multipleApprovalRulesAvailable: parseBoolean(multipleApprovalRulesAvailable),
},
render: (createElement) =>
createElement(SidebarReviewers, {
props: {
......
......@@ -11,3 +11,5 @@ class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
end
end
end
MergeRequestSidebarBasicEntity.prepend_mod_with('MergeRequestSidebarBasicEntity')
<script>
import { n__, sprintf, s__ } from '~/locale';
import { getApprovalRuleNamesLeft } from 'ee/vue_merge_request_widget/mappers';
import { toNounSeriesText } from '~/lib/utils/grammar';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import approvalSummaryQuery from '../../queries/approval_summary.query.graphql';
import approvalSummarySubscription from '../../queries/approval_summary.subscription.graphql';
export default {
apollo: {
mergeRequest: {
query: approvalSummaryQuery,
variables() {
return {
projectPath: this.projectPath,
iid: this.issuableIid,
};
},
update: (data) => data.project?.mergeRequest,
subscribeToMore: {
document: approvalSummarySubscription,
variables() {
return {
issuableId: convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.issuableId),
};
},
updateQuery(
_,
{
subscriptionData: {
data: { mergeRequestApprovalStateUpdated: queryResult },
},
},
) {
if (queryResult) {
this.mergeRequest = queryResult;
}
},
},
},
},
inject: ['projectPath', 'issuableId', 'issuableIid', 'multipleApprovalRulesAvailable'],
data() {
return {
mergeRequest: null,
};
},
computed: {
isLoading() {
return this.$apollo?.queries?.mergeRequest?.loading;
},
approvalsOptional() {
return (
this.mergeRequest.approvalsRequired === 0 && this.mergeRequest.approvedBy.nodes.length === 0
);
},
approvalsLeft() {
return this.mergeRequest.approvalsLeft || 0;
},
rulesLeft() {
return getApprovalRuleNamesLeft(
this.multipleApprovalRulesAvailable,
(this.mergeRequest.approvalState?.rules || []).filter((r) => !r.approved),
);
},
approvalsLeftMessage() {
if (this.approvalsOptional) {
return s__('mrWidget|Approval is optional');
}
if (this.rulesLeft.length) {
return sprintf(
n__(
'Requires %{count} approval from %{names}.',
'Requires %{count} approvals from %{names}.',
this.approvalsLeft,
),
{
names: toNounSeriesText(this.rulesLeft),
count: this.approvalsLeft,
},
false,
);
}
return n__(
'Requires %d approval from eligible users.',
'Requires %d approvals from eligible users.',
this.approvalsLeft,
);
},
},
};
</script>
<template>
<div
v-if="isLoading"
class="gl-animate-skeleton-loader gl-mt-3 gl-h-4 gl-rounded-base gl-w-full"
></div>
<p v-else-if="mergeRequest" :class="{ 'text-muted': approvalsOptional }" class="gl-mt-3 gl-mb-0">
{{ approvalsLeftMessage }}
</p>
</template>
query approvalSummary($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
id
approvalsLeft
approvalsRequired
approvedBy {
nodes {
id
}
}
approvalState {
rules {
id
approved
approvalsRequired
name
type
}
}
}
}
}
subscription approvalSummarySubscription($issuableId: IssuableID!) {
mergeRequestApprovalStateUpdated(issuableId: $issuableId) {
... on MergeRequest {
id
approvalsLeft
approvalsRequired
approvedBy {
nodes {
id
}
}
approvalState {
rules {
id
approved
approvalsRequired
name
type
}
}
}
}
}
......@@ -8,7 +8,8 @@ module IssuablesHelper
def issuable_sidebar_options(sidebar_data)
super.merge(
weightOptions: ::Issue.weight_options,
weightNoneValue: ::Issue::WEIGHT_NONE
weightNoneValue: ::Issue::WEIGHT_NONE,
multipleApprovalRulesAvailable: sidebar_data[:multiple_approval_rules_available]
)
end
......
# frozen_string_literal: true
module EE
module MergeRequestSidebarBasicEntity
extend ActiveSupport::Concern
prepended do
expose :multiple_approval_rules_available do |merge_request|
merge_request.target_project.multiple_approval_rules_available?
end
end
end
end
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ApprovalSummary from 'ee/merge_requests/components/reviewers/approval_summary.vue';
import approvalSummaryQuery from 'ee/merge_requests/queries/approval_summary.query.graphql';
import approvalSummarySubscription from 'ee/merge_requests/queries/approval_summary.subscription.graphql';
Vue.use(VueApollo);
const mockData = ({ approvalsRequired = 1, approvalsLeft = 1, approvedBy = [] } = {}) => ({
data: {
project: {
id: 1,
mergeRequest: {
id: 1,
approvalsLeft,
approvalsRequired,
approvedBy: {
nodes: approvedBy,
},
approvalState: {
rules: [
{
id: 1,
approved: false,
approvalsRequired,
name: 'Frontend',
type: 'CODE_OWNER',
},
],
},
},
},
},
});
describe('Reviewers drawer approval summary component', () => {
let wrapper;
let apolloProvider;
let mockedSubscription;
const createComponent = ({
multipleApprovalRulesAvailable = true,
resolver = jest.fn().mockResolvedValue(mockData()),
} = {}) => {
mockedSubscription = createMockApolloSubscription();
apolloProvider = createMockApollo([[approvalSummaryQuery, resolver]]);
apolloProvider.defaultClient.setRequestHandler(
approvalSummarySubscription,
() => mockedSubscription,
);
wrapper = shallowMount(ApprovalSummary, {
apolloProvider,
provide: {
projectPath: 'project-path',
issuableId: '1',
issuableIid: '1',
multipleApprovalRulesAvailable,
},
});
};
it('renders loading skeleton', () => {
createComponent();
expect(wrapper.classes()).toContain('gl-animate-skeleton-loader');
});
describe('when approval is required', () => {
it('renders approval summary', async () => {
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe('Requires 1 approval from Code Owners.');
});
});
describe('when approval is optional', () => {
it('renders optional approval summary', async () => {
createComponent({
resolver: jest.fn().mockResolvedValue(mockData({ approvalsRequired: 0 })),
});
await waitForPromises();
expect(wrapper.text()).toBe('Approval is optional');
});
});
it('updates text when subscription updates', async () => {
createComponent();
await waitForPromises();
mockedSubscription.next({
data: {
mergeRequestApprovalStateUpdated: mockData({
approvalsLeft: 0,
approvalsRequired: 0,
approvedBy: [{ id: 1 }],
}).data.project.mergeRequest,
},
});
await waitForPromises();
expect(wrapper.text()).toBe('Requires 0 approvals from Code Owners.');
});
describe('when multipleApprovalRulesAvailable is false', () => {
it('renders approval summary', async () => {
createComponent({ multipleApprovalRulesAvailable: false });
await waitForPromises();
expect(wrapper.text()).toBe('Requires 1 approval from eligible users.');
});
});
});
......@@ -10,6 +10,12 @@ let wrapper;
function createComponent(propsData = {}) {
wrapper = shallowMount(ReviewerDrawer, {
propsData,
provide: {
projectPath: 'gitlab-org/gitlab',
issuableId: '1',
issuableIid: '1',
multipleApprovalRulesAvailable: false,
},
});
}
......
......@@ -16,6 +16,10 @@ describe('ReviewerTitle component', () => {
...props,
},
provide: {
projectPath: 'gitlab-org/gitlab',
issuableId: '1',
issuableIid: '1',
multipleApprovalRulesAvailable: false,
glFeatures: {
reviewerAssignDrawer,
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment