Skip to content
Snippets Groups Projects
Verified Commit 0c8002a2 authored by Phil Hughes's avatar Phil Hughes
Browse files

Added approval summary header to reviewer drawer component

parent 9f04c223
No related branches found
No related tags found
1 merge request!148384Added approval summary header to reviewer drawer component
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