Commit a88561a7 authored by Robert Hunt's avatar Robert Hunt Committed by Natalia Tepluhina

Refactored the rule_form.vue validation code structure

parent 413a6a8e
...@@ -51,7 +51,7 @@ export default { ...@@ -51,7 +51,7 @@ export default {
}, },
}, },
data() { data() {
const defaults = { return {
name: this.defaultRuleName, name: this.defaultRuleName,
approvalsRequired: 1, approvalsRequired: 1,
minApprovalsRequired: 0, minApprovalsRequired: 0,
...@@ -67,19 +67,9 @@ export default { ...@@ -67,19 +67,9 @@ export default {
ruleType: null, ruleType: null,
...this.getInitialData(), ...this.getInitialData(),
}; };
return defaults;
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
showApproverTypeSelect() {
return (
this.glFeatures.ffComplianceApprovalGates &&
!this.isEditing &&
!this.isMrEdit &&
!READONLY_NAMES.includes(this.name)
);
},
isExternalApprovalRule() { isExternalApprovalRule() {
return this.ruleType === RULE_TYPE_EXTERNAL_APPROVAL; return this.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
}, },
...@@ -102,83 +92,84 @@ export default { ...@@ -102,83 +92,84 @@ export default {
groupIds() { groupIds() {
return this.groups.map((x) => x.id); return this.groups.map((x) => x.id);
}, },
validation() {
if (!this.showValidation) {
return {};
}
const invalidObject = {
name: this.invalidName,
};
if (!this.isMrEdit) {
invalidObject.branches = this.invalidBranches;
}
if (this.isExternalApprovalRule) {
invalidObject.externalUrl = this.invalidApprovalGateUrl;
} else {
invalidObject.approvers = this.invalidApprovers;
invalidObject.approvalsRequired = this.invalidApprovalsRequired;
}
return invalidObject;
},
invalidApprovalGateUrl() { invalidApprovalGateUrl() {
let error = '';
if (this.serverValidationErrors.includes('External url has already been taken')) { if (this.serverValidationErrors.includes('External url has already been taken')) {
error = __('External url has already been taken'); return this.$options.i18n.validations.externalUrlTaken;
} else if (!this.externalUrl || !isSafeURL(this.externalUrl)) { } else if (!this.externalUrl || !isSafeURL(this.externalUrl)) {
error = __('Please provide a valid URL'); return this.$options.i18n.validations.invalidUrl;
} }
return error; return '';
}, },
invalidName() { invalidName() {
let error = '';
if (this.isMultiSubmission) { if (this.isMultiSubmission) {
if (this.serverValidationErrors.includes('name has already been taken')) { if (this.serverValidationErrors.includes('name has already been taken')) {
error = __('Rule name is already taken.'); return this.$options.i18n.validations.ruleNameTaken;
} else if (!this.name) { }
error = __('Please provide a name');
if (!this.name) {
return this.$options.i18n.validations.ruleNameMissing;
} }
} }
return error; return '';
}, },
invalidApprovalsRequired() { invalidApprovalsRequired() {
if (!isNumber(this.approvalsRequired)) { if (!isNumber(this.approvalsRequired)) {
return __('Please enter a valid number'); return this.$options.i18n.validations.approvalsRequiredNotNumber;
} }
if (this.approvalsRequired < 0) { if (this.approvalsRequired < 0) {
return __('Please enter a non-negative number'); return this.$options.i18n.validations.approvalsRequiredNegativeNumber;
} }
return this.approvalsRequired < this.minApprovalsRequired if (this.approvalsRequired < this.minApprovalsRequired) {
? sprintf(__('Please enter a number greater than %{number} (from the project settings)'), { return sprintf(this.$options.i18n.validations.approvalsRequiredMinimum, {
number: this.minApprovalsRequired, number: this.minApprovalsRequired,
}) });
: ''; }
return '';
}, },
invalidApprovers() { invalidApprovers() {
if (!this.isMultiSubmission) { if (this.isMultiSubmission && this.approvers.length <= 0) {
return ''; return this.$options.i18n.validations.approversRequired;
} }
return !this.approvers.length ? __('Please select and add a member') : ''; return '';
}, },
invalidBranches() { invalidBranches() {
if (this.isMrEdit) return ''; if (!this.isMrEdit && this.branches.some((id) => typeof id !== 'number')) {
return this.$options.i18n.validations.branchesRequired;
const invalidTypes = this.branches.filter((id) => typeof id !== 'number'); }
return invalidTypes.length ? __('Please select a valid target branch') : ''; return '';
}, },
isValid() { isValid() {
return Object.keys(this.validation).every((key) => !this.validation[key]); return (
this.isValidName &&
this.isValidBranches &&
this.isValidApprovalsRequired &&
this.isValidApprovers
);
},
isValidExternalApprovalRule() {
return this.isValidName && this.isValidBranches && this.isValidApprovalGateUrl;
},
isValidName() {
return !this.showValidation || !this.invalidName;
},
isValidBranches() {
return !this.showValidation || !this.invalidBranches;
},
isValidApprovalsRequired() {
return !this.showValidation || !this.invalidApprovalsRequired;
},
isValidApprovers() {
return !this.showValidation || !this.invalidApprovers;
},
isValidApprovalGateUrl() {
return !this.showValidation || !this.invalidApprovalGateUrl;
}, },
isMultiSubmission() { isMultiSubmission() {
return this.settings.allowMultiRule && !this.isFallbackSubmission; return this.settings.allowMultiRule && !this.isFallbackSubmission;
...@@ -191,7 +182,15 @@ export default { ...@@ -191,7 +182,15 @@ export default {
isPersisted() { isPersisted() {
return this.initRule && this.initRule.id; return this.initRule && this.initRule.id;
}, },
isNameVisible() { showApproverTypeSelect() {
return (
this.glFeatures.ffComplianceApprovalGates &&
!this.isEditing &&
!this.isMrEdit &&
!READONLY_NAMES.includes(this.name)
);
},
showName() {
return !this.settings.lockedApprovalsRuleName; return !this.settings.lockedApprovalsRuleName;
}, },
isNameDisabled() { isNameDisabled() {
...@@ -199,6 +198,9 @@ export default { ...@@ -199,6 +198,9 @@ export default {
Boolean(this.isPersisted || this.defaultRuleName) && READONLY_NAMES.includes(this.name) Boolean(this.isPersisted || this.defaultRuleName) && READONLY_NAMES.includes(this.name)
); );
}, },
showProtectedBranch() {
return !this.isMrEdit && this.settings.allowMultiRule;
},
removeHiddenGroups() { removeHiddenGroups() {
return this.containsHiddenGroups && !this.approversByType[TYPE_HIDDEN_GROUPS]; return this.containsHiddenGroups && !this.approversByType[TYPE_HIDDEN_GROUPS];
}, },
...@@ -227,11 +229,9 @@ export default { ...@@ -227,11 +229,9 @@ export default {
externalUrl: this.externalUrl, externalUrl: this.externalUrl,
}; };
}, },
showProtectedBranch() {
return !this.isMrEdit && this.settings.allowMultiRule;
},
approvalGateLabel() { approvalGateLabel() {
return this.isEditing ? this.$options.i18n.approvalGate : this.$options.i18n.addApprovalGate; const { approvalGate, addApprovalGate } = this.$options.i18n.form;
return this.isEditing ? approvalGate : addApprovalGate;
}, },
}, },
watch: { watch: {
...@@ -270,8 +270,11 @@ export default { ...@@ -270,8 +270,11 @@ export default {
let submission; let submission;
this.serverValidationErrors = []; this.serverValidationErrors = [];
this.showValidation = true;
if (!this.validate()) { const valid = this.isExternalApprovalRule ? this.isValidExternalApprovalRule : this.isValid;
if (!valid) {
submission = Promise.resolve(); submission = Promise.resolve();
} else if (this.isFallbackSubmission) { } else if (this.isFallbackSubmission) {
submission = this.submitFallback(); submission = this.submitFallback();
...@@ -332,11 +335,6 @@ export default { ...@@ -332,11 +335,6 @@ export default {
return Promise.all([this.submitFallback(), id ? this.deleteRule(id) : Promise.resolve()]); return Promise.all([this.submitFallback(), id ? this.deleteRule(id) : Promise.resolve()]);
}, },
validate() {
this.showValidation = true;
return this.isValid;
},
getInitialData() { getInitialData() {
if (!this.initRule || this.defaultRuleName) { if (!this.initRule || this.defaultRuleName) {
return {}; return {};
...@@ -381,8 +379,33 @@ export default { ...@@ -381,8 +379,33 @@ export default {
}, },
}, },
i18n: { i18n: {
approvalGate: s__('ApprovalRule|Approvel gate'), form: {
addApprovalGate: s__('ApprovalRule|Add approvel gate'), addApprovalGate: s__('ApprovalRule|Add approvel gate'),
approvalGate: s__('ApprovalRule|Approvel gate'),
approvalGateDescription: s__('ApprovalRule|Invoke an external API as part of the approvals'),
approvalsRequiredLabel: s__('ApprovalRule|Approvals required'),
approvalTypeLabel: s__('ApprovalRule|Approver Type'),
approversLabel: s__('ApprovalRule|Add approvers'),
nameLabel: s__('ApprovalRule|Rule name'),
nameDescription: s__('ApprovalRule|Examples: QA, Security.'),
protectedBranchLabel: s__('ApprovalRule|Target branch'),
protectedBranchDescription: __(
'Apply this approval rule to any branch or a specific protected branch.',
),
},
validations: {
approvalsRequiredNegativeNumber: __('Please enter a non-negative number'),
approvalsRequiredNotNumber: __('Please enter a valid number'),
approvalsRequiredMinimum: __(
'Please enter a number greater than %{number} (from the project settings)',
),
approversRequired: __('Please select and add a member'),
branchesRequired: __('Please select a valid target branch'),
ruleNameTaken: __('Rule name is already taken.'),
ruleNameMissing: __('Please provide a name'),
externalUrlTaken: __('External url has already been taken'),
invalidUrl: __('Please provide a valid URL'),
},
}, },
approverTypeOptions: [ approverTypeOptions: [
{ type: RULE_TYPE_USER_OR_GROUP_APPROVER, text: s__('ApprovalRule|Users or groups') }, { type: RULE_TYPE_USER_OR_GROUP_APPROVER, text: s__('ApprovalRule|Users or groups') },
...@@ -393,80 +416,84 @@ export default { ...@@ -393,80 +416,84 @@ export default {
<template> <template>
<form novalidate @submit.prevent.stop="submit"> <form novalidate @submit.prevent.stop="submit">
<div v-if="isNameVisible" class="form-group gl-form-group"> <div v-if="showName" class="form-group gl-form-group">
<label class="col-form-label">{{ s__('ApprovalRule|Rule name') }}</label> <label class="col-form-label">{{ $options.i18n.form.nameLabel }}</label>
<input <input
v-model="name" v-model="name"
:class="{ 'is-invalid': validation.name }" :class="{ 'is-invalid': !isValidName }"
:disabled="isNameDisabled" :disabled="isNameDisabled"
class="gl-form-input form-control" class="gl-form-input form-control"
name="name" name="name"
type="text" type="text"
data-qa-selector="rule_name_field" data-qa-selector="rule_name_field"
/> />
<span class="invalid-feedback">{{ validation.name }}</span> <span class="invalid-feedback">{{ isValidName ? '' : invalidName }}</span>
<small class="form-text text-gl-muted"> <small class="form-text text-gl-muted">
{{ s__('ApprovalRule|Examples: QA, Security.') }} {{ $options.i18n.form.nameDescription }}
</small> </small>
</div> </div>
<div v-if="showProtectedBranch" class="form-group gl-form-group"> <div v-if="showProtectedBranch" class="form-group gl-form-group">
<label class="col-form-label">{{ s__('ApprovalRule|Target branch') }}</label> <label class="col-form-label">{{ $options.i18n.form.protectedBranchLabel }}</label>
<branches-select <branches-select
v-model="branchesToAdd" v-model="branchesToAdd"
:project-id="settings.projectId" :project-id="settings.projectId"
:is-invalid="Boolean(validation.branches)" :is-invalid="!isValidBranches"
:init-rule="rule" :init-rule="rule"
/> />
<span class="invalid-feedback">{{ validation.branches }}</span> <span class="invalid-feedback">{{ isValidBranches ? '' : invalidBranches }}</span>
<small class="form-text text-gl-muted"> <small class="form-text text-gl-muted">
{{ __('Apply this approval rule to any branch or a specific protected branch.') }} {{ $options.i18n.form.protectedBranchDescription }}
</small> </small>
</div> </div>
<div v-if="showApproverTypeSelect" class="form-group gl-form-group"> <div v-if="showApproverTypeSelect" class="form-group gl-form-group">
<label class="col-form-label">{{ s__('ApprovalRule|Approver Type') }}</label> <label class="col-form-label">{{ $options.i18n.form.approvalTypeLabel }}</label>
<approver-type-select <approver-type-select
v-model="ruleType" v-model="ruleType"
:approver-type-options="$options.approverTypeOptions" :approver-type-options="$options.approverTypeOptions"
/> />
</div> </div>
<div v-if="!isExternalApprovalRule" class="form-group gl-form-group"> <div v-if="!isExternalApprovalRule" class="form-group gl-form-group">
<label class="col-form-label">{{ s__('ApprovalRule|Approvals required') }}</label> <label class="col-form-label">{{ $options.i18n.form.approvalsRequiredLabel }}</label>
<input <input
v-model.number="approvalsRequired" v-model.number="approvalsRequired"
:class="{ 'is-invalid': validation.approvalsRequired }" :class="{ 'is-invalid': !isValidApprovalsRequired }"
class="gl-form-input form-control mw-6em" class="gl-form-input form-control mw-6em"
name="approvals_required" name="approvals_required"
type="number" type="number"
:min="minApprovalsRequired" :min="minApprovalsRequired"
data-qa-selector="approvals_required_field" data-qa-selector="approvals_required_field"
/> />
<span class="invalid-feedback">{{ validation.approvalsRequired }}</span> <span class="invalid-feedback">{{
isValidApprovalsRequired ? '' : invalidApprovalsRequired
}}</span>
</div> </div>
<div v-if="!isExternalApprovalRule" class="form-group gl-form-group"> <div v-if="!isExternalApprovalRule" class="form-group gl-form-group">
<label class="col-form-label">{{ s__('ApprovalRule|Add approvers') }}</label> <label class="col-form-label">{{ $options.i18n.form.approversLabel }}</label>
<approvers-select <approvers-select
v-model="approversToAdd" v-model="approversToAdd"
:project-id="settings.projectId" :project-id="settings.projectId"
:skip-user-ids="userIds" :skip-user-ids="userIds"
:skip-group-ids="groupIds" :skip-group-ids="groupIds"
:is-invalid="Boolean(validation.approvers)" :is-invalid="!isValidApprovers"
data-qa-selector="member_select_field" data-qa-selector="member_select_field"
/> />
<span class="invalid-feedback">{{ validation.approvers }}</span> <span class="invalid-feedback">{{ isValidApprovers ? '' : invalidApprovers }}</span>
</div> </div>
<div v-if="isExternalApprovalRule" class="form-group gl-form-group"> <div v-if="isExternalApprovalRule" class="form-group gl-form-group">
<label class="col-form-label">{{ approvalGateLabel }}</label> <label class="col-form-label">{{ approvalGateLabel }}</label>
<input <input
v-model="externalUrl" v-model="externalUrl"
:class="{ 'is-invalid': validation.externalUrl }" :class="{ 'is-invalid': !isValidApprovalGateUrl }"
class="gl-form-input form-control" class="gl-form-input form-control"
name="approval_gate_url" name="approval_gate_url"
type="url" type="url"
data-qa-selector="external_url_field" data-qa-selector="external_url_field"
/> />
<span class="invalid-feedback">{{ validation.externalUrl }}</span> <span class="invalid-feedback">{{
isValidApprovalGateUrl ? '' : invalidApprovalGateUrl
}}</span>
<small class="form-text text-gl-muted"> <small class="form-text text-gl-muted">
{{ s__('ApprovalRule|Invoke an external API as part of the approvals') }} {{ $options.i18n.form.approvalGateDescription }}
</small> </small>
</div> </div>
<div v-if="!isExternalApprovalRule" class="bordered-box overflow-auto h-12em"> <div v-if="!isExternalApprovalRule" class="bordered-box overflow-auto h-12em">
......
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