Commit 248f26b5 authored by Phil Hughes's avatar Phil Hughes

Added human readable summary text to approval rules

Adds human readable summary text to the collapsed
state in the merge request form.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/283972
parent 706d840b
<script>
import { uniqueId } from 'lodash';
import { GlIcon, GlButton, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui';
import {
GlIcon,
GlButton,
GlCollapse,
GlCollapseToggleDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, n__, sprintf } from '~/locale';
import App from '../app.vue';
import MrRules from './mr_rules.vue';
import MrRulesHiddenInputs from './mr_rules_hidden_inputs.vue';
......@@ -16,7 +25,9 @@ export default {
},
directives: {
CollapseToggle: GlCollapseToggleDirective,
SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
data() {
return {
collapseId: uniqueId('approval-rules-expandable-section-'),
......@@ -24,11 +35,79 @@ export default {
};
},
computed: {
...mapState({
rules: state => state.approvals.rules,
canOverride: state => state.settings.canOverride,
}),
toggleIcon() {
return this.isCollapsed ? 'chevron-down' : 'chevron-right';
},
isCollapseFeatureEnabled() {
return gon.features?.mergeRequestReviewers && gon.features?.mrCollapsedApprovalRules;
return this.glFeatures.mergeRequestReviewers && this.glFeatures.mrCollapsedApprovalRules;
},
hasOptionalRules() {
return this.rules.every(r => r.approvalsRequired === 0);
},
requiredRules() {
return this.rules.reduce((acc, rule) => {
if (rule.approvalsRequired > 0) {
acc.push(rule);
}
return acc;
}, []);
},
collapsedSummary() {
const rulesLength = this.requiredRules.length;
const firstRule = this.requiredRules[0];
if (this.hasOptionalRules) {
return __('Approvals are optional.');
} else if (rulesLength === 1 && firstRule.ruleType === 'any_approver') {
return sprintf(
n__(
'%{strong_start}%{count} member%{strong_end} must approve to merge. Anyone with role Developer or higher can approve.',
'%{strong_start}%{count} members%{strong_end} must approve to merge. Anyone with role Developer or higher can approve.',
firstRule.approvalsRequired,
),
{
strong_start: '<strong>',
strong_end: '</strong>',
count: firstRule.approvalsRequired,
},
false,
);
} else if (rulesLength === 1 && firstRule.ruleType !== 'any_approver') {
return sprintf(
n__(
'%{strong_start}%{count} eligible member%{strong_end} must approve to merge.',
'%{strong_start}%{count} eligible members%{strong_end} must approve to merge.',
firstRule.approvalsRequired,
),
{
strong_start: '<strong>',
strong_end: '</strong>',
count: firstRule.approvalsRequired,
},
false,
);
} else if (rulesLength > 1) {
return sprintf(
n__(
'%{strong_start}%{count} approval rule%{strong_end} requires eligible members to approve before merging.',
'%{strong_start}%{count} approval rules%{strong_end} require eligible members to approve before merging.',
rulesLength,
),
{
strong_start: '<strong>',
strong_end: '</strong>',
count: rulesLength,
},
false,
);
}
return null;
},
},
};
......@@ -36,7 +115,17 @@ export default {
<template>
<div v-if="isCollapseFeatureEnabled" class="gl-mt-2">
<gl-button v-collapse-toggle="collapseId" variant="link" button-text-classes="flex">
<p
v-safe-html="collapsedSummary"
class="gl-mb-0 gl-text-gray-500"
data-testid="collapsedSummaryText"
></p>
<gl-button
v-if="canOverride"
v-collapse-toggle="collapseId"
variant="link"
button-text-classes="flex"
>
<gl-icon :name="toggleIcon" class="mr-1" />
<span>{{ s__('ApprovalRule|Approval rules') }}</span>
</gl-button>
......
......@@ -20,9 +20,14 @@ export default {
computed: {
...mapState(['settings']),
},
created() {
this.onInputChangeDebounced = debounce(event => {
this.onInputChange(event);
}, 1000);
},
methods: {
...mapActions(['putRule', 'postRule']),
onInputChange: debounce(function debounceSearch(event) {
onInputChange(event) {
const { value } = event.target;
const approvalsRequired = parseInt(value, 10);
......@@ -35,7 +40,7 @@ export default {
approvalsRequired,
});
}
}, 1000),
},
},
};
</script>
......@@ -48,6 +53,6 @@ export default {
type="number"
:min="rule.minApprovalsRequired || 0"
data-qa-selector="approvals_number_field"
@input="onInputChange"
@input="onInputChangeDebounced"
/>
</template>
......@@ -21,6 +21,7 @@ export default function mountApprovalInput(el) {
prefix: 'mr-edit',
canEdit: parseBoolean(el.dataset.canEdit),
allowMultiRule: parseBoolean(el.dataset.allowMultiRule),
canOverride: parseBoolean(el.dataset.canOverride),
});
store.dispatch('setTargetBranch', targetBranch);
......
......@@ -6,6 +6,7 @@
#js-mr-approvals-input{ data: { 'project_id': @target_project.id,
'can_edit': can?(current_user, :update_approvers, issuable).to_s,
'allow_multi_rule': @target_project.multiple_approval_rules_available?.to_s,
'can-override': @target_project.can_override_approvers?.to_s,
'mr_id': issuable.iid,
'mr_settings_path': presenter.api_approval_settings_path,
'eligible_approvers_docs_path': help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'),
......
......@@ -16,10 +16,16 @@ describe('EE Approvals MREditApp', () => {
let store;
let axiosMock;
const factory = () => {
const factory = (mergeRequestReviewers = false, mrCollapsedApprovalRules = false) => {
wrapper = mount(MREditApp, {
localVue,
store: new Vuex.Store(store),
provide: {
glFeatures: {
mergeRequestReviewers,
mrCollapsedApprovalRules,
},
},
});
};
......@@ -57,16 +63,19 @@ describe('EE Approvals MREditApp', () => {
});
describe('with rules', () => {
beforeEach(() => {
store.modules.approvals.state.rules = [{ id: 7, approvers: [] }];
factory();
});
beforeEach(() => {});
it('renders MR rules', () => {
store.modules.approvals.state.rules = [{ id: 7, approvers: [] }];
factory();
expect(wrapper.find(MRRules).findAll('.js-name')).toHaveLength(1);
});
it('renders hidden inputs', () => {
store.modules.approvals.state.rules = [{ id: 7, approvers: [] }];
factory();
expect(
wrapper
.find('.js-approval-rules')
......@@ -74,5 +83,83 @@ describe('EE Approvals MREditApp', () => {
.exists(),
).toBe(true);
});
describe('summary text', () => {
const findSummaryText = () => wrapper.find('[data-testid="collapsedSummaryText"]');
it('optional approvals', () => {
store.modules.approvals.state.rules = [];
factory(true, true);
expect(findSummaryText().text()).toEqual('Approvals are optional.');
});
it('multiple optional approval rules', () => {
store.modules.approvals.state.rules = [
{ ruleType: 'any_approver', approvalsRequired: 0 },
{ ruleType: 'regular', approvalsRequired: 0, approvers: [] },
];
factory(true, true);
expect(findSummaryText().text()).toEqual('Approvals are optional.');
});
it('anyone can approve', () => {
store.modules.approvals.state.rules = [
{
ruleType: 'any_approver',
approvalsRequired: 1,
},
];
factory(true, true);
expect(findSummaryText().text()).toEqual(
'1 member must approve to merge. Anyone with role Developer or higher can approve.',
);
});
it('2 required approval', () => {
store.modules.approvals.state.rules = [
{
ruleType: 'any_approver',
approvalsRequired: 1,
},
{
ruleType: 'regular',
approvalsRequired: 1,
approvers: [],
},
];
factory(true, true);
expect(findSummaryText().text()).toEqual(
'2 approval rules require eligible members to approve before merging.',
);
});
it('multiple required approval', () => {
store.modules.approvals.state.rules = [
{
ruleType: 'any_approver',
approvalsRequired: 1,
},
{
ruleType: 'regular',
approvalsRequired: 1,
approvers: [],
},
{
ruleType: 'regular',
approvalsRequired: 2,
approvers: [],
},
];
factory(true, true);
expect(findSummaryText().text()).toEqual(
'3 approval rules require eligible members to approve before merging.',
);
});
});
});
});
......@@ -792,6 +792,21 @@ msgid_plural "%{strong_start}%{commit_count}%{strong_end} Commits"
msgstr[0] ""
msgstr[1] ""
msgid "%{strong_start}%{count} approval rule%{strong_end} requires eligible members to approve before merging."
msgid_plural "%{strong_start}%{count} approval rules%{strong_end} require eligible members to approve before merging."
msgstr[0] ""
msgstr[1] ""
msgid "%{strong_start}%{count} eligible member%{strong_end} must approve to merge."
msgid_plural "%{strong_start}%{count} eligible members%{strong_end} must approve to merge."
msgstr[0] ""
msgstr[1] ""
msgid "%{strong_start}%{count} member%{strong_end} must approve to merge. Anyone with role Developer or higher can approve."
msgid_plural "%{strong_start}%{count} members%{strong_end} must approve to merge. Anyone with role Developer or higher can approve."
msgstr[0] ""
msgstr[1] ""
msgid "%{strong_start}%{human_size}%{strong_end} Files"
msgstr ""
......@@ -3599,6 +3614,9 @@ msgstr ""
msgid "ApprovalStatusTooltip|Fails to adhere to separation of duties"
msgstr ""
msgid "Approvals are optional."
msgstr ""
msgid "Approvals|Section: %section"
msgstr ""
......
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