[ACA-3742] Metadata - constraints validation (#5908)

* card min max value validator

* card match value validator

* card value length validator

* map validators to constraint type

* add minmax

* update exported validators

* register validators based on constraint type

* translate errors with parameters

* tests
This commit is contained in:
Cilibiu Bogdan
2020-07-24 18:58:07 +03:00
committed by GitHub
parent 154ca07aad
commit aa3d890342
15 changed files with 223 additions and 12 deletions

View File

@@ -129,7 +129,8 @@
"uploadfileform", "uploadfileform",
"processwithstarteventform", "processwithstarteventform",
"processstring", "processstring",
"typeahed" "typeahed",
"minmax"
], ],
"dictionaries": [ "dictionaries": [
"html", "html",

View File

@@ -55,7 +55,7 @@
class="adf-textitem-editable-error" class="adf-textitem-editable-error"
*ngIf="hasErrors"> *ngIf="hasErrors">
<ul> <ul>
<li *ngFor="let errorMessage of errorMessages">{{ errorMessage | translate }}</li> <li *ngFor="let error of errors">{{ error.message | translate: error }}</li>
</ul> </ul>
</mat-error> </mat-error>

View File

@@ -27,6 +27,7 @@ import { MatChipsModule } from '@angular/material/chips';
import { ClipboardService } from '../../../clipboard/clipboard.service'; import { ClipboardService } from '../../../clipboard/clipboard.service';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CardViewItemValidator } from '../../interfaces/card-view-item-validator.interface';
describe('CardViewTextItemComponent', () => { describe('CardViewTextItemComponent', () => {
@@ -441,7 +442,7 @@ describe('CardViewTextItemComponent', () => {
}); });
it('should set the errorMessages properly if the editedValue is invalid', async () => { it('should set the errorMessages properly if the editedValue is invalid', async () => {
const expectedErrorMessages = ['Something went wrong']; const expectedErrorMessages = [{ message: 'Something went wrong' } as CardViewItemValidator];
component.property.isValid = () => false; component.property.isValid = () => false;
component.property.getValidationErrors = () => expectedErrorMessages; component.property.getValidationErrors = () => expectedErrorMessages;
@@ -450,7 +451,7 @@ describe('CardViewTextItemComponent', () => {
await fixture.whenStable(); await fixture.whenStable();
updateTextField(component.property.key, 'updated-value'); updateTextField(component.property.key, 'updated-value');
expect(component.errorMessages).toBe(expectedErrorMessages); expect(component.errors).toBe(expectedErrorMessages);
}); });
it('should update the property value after a successful update attempt', async () => { it('should update the property value after a successful update attempt', async () => {

View File

@@ -22,6 +22,7 @@ import { BaseCardView } from '../base-card-view';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { ClipboardService } from '../../../clipboard/clipboard.service'; import { ClipboardService } from '../../../clipboard/clipboard.service';
import { TranslationService } from '../../../services/translation.service'; import { TranslationService } from '../../../services/translation.service';
import { CardViewItemValidator } from '../../interfaces/card-view-item-validator.interface';
export const DEFAULT_SEPARATOR = ', '; export const DEFAULT_SEPARATOR = ', ';
const templateTypes = { const templateTypes = {
@@ -55,7 +56,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
multiValueSeparator: string = DEFAULT_SEPARATOR; multiValueSeparator: string = DEFAULT_SEPARATOR;
editedValue: string | string[]; editedValue: string | string[];
errorMessages: string[]; errors: CardViewItemValidator[];
templateType: string; templateType: string;
constructor(cardViewUpdateService: CardViewUpdateService, constructor(cardViewUpdateService: CardViewUpdateService,
@@ -92,7 +93,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
} }
private resetErrorMessages() { private resetErrorMessages() {
this.errorMessages = []; this.errors = [];
} }
update(): void { update(): void {
@@ -102,7 +103,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
this.property.value = updatedValue; this.property.value = updatedValue;
this.resetErrorMessages(); this.resetErrorMessages();
} else { } else {
this.errorMessages = this.property.getValidationErrors(this.editedValue); this.errors = this.property.getValidationErrors(this.editedValue);
} }
} }
@@ -176,7 +177,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
} }
get hasErrors(): boolean { get hasErrors(): boolean {
return this.errorMessages && this.errorMessages.length > 0; return (!!this.errors?.length) ?? false;
} }
get isChipViewEnabled(): boolean { get isChipViewEnabled(): boolean {

View File

@@ -91,7 +91,7 @@ describe('CardViewBaseItemModel', () => {
const isValid = itemModel.isValid('test-against-this'); const isValid = itemModel.isValid('test-against-this');
expect(isValid).toBe(false); expect(isValid).toBe(false);
expect(itemModel.getValidationErrors('test-against-this')).toEqual(['validator 1', 'validator 3']); expect(itemModel.getValidationErrors('test-against-this')).toEqual([validator1, validator3 ]);
}); });
}); });
}); });

View File

@@ -16,6 +16,7 @@
*/ */
import { CardViewItemProperties, CardViewItemValidator } from '../interfaces/card-view.interfaces'; import { CardViewItemProperties, CardViewItemValidator } from '../interfaces/card-view.interfaces';
import validatorsMap from '../validators/validators.map';
export abstract class CardViewBaseItemModel { export abstract class CardViewBaseItemModel {
label: string; label: string;
@@ -38,6 +39,14 @@ export abstract class CardViewBaseItemModel {
this.icon = cardViewItemProperties.icon || ''; this.icon = cardViewItemProperties.icon || '';
this.validators = cardViewItemProperties.validators || []; this.validators = cardViewItemProperties.validators || [];
this.data = cardViewItemProperties.data || null; this.data = cardViewItemProperties.data || null;
if (cardViewItemProperties?.constraints?.length ?? 0) {
for (const constraint of cardViewItemProperties.constraints) {
if (constraint.type !== 'LIST') {
this.validators.push(validatorsMap[constraint.type.toLowerCase()](constraint.parameters));
}
}
}
} }
isEmpty(): boolean { isEmpty(): boolean {
@@ -54,11 +63,11 @@ export abstract class CardViewBaseItemModel {
.reduce((isValidUntilNow, isValid) => isValidUntilNow && isValid, true); .reduce((isValidUntilNow, isValid) => isValidUntilNow && isValid, true);
} }
getValidationErrors(value): string[] { getValidationErrors(value): CardViewItemValidator[] {
if (!this.validators.length) { if (!this.validators.length) {
return []; return [];
} }
return this.validators.filter((validator) => !validator.isValid(value)).map((validator) => validator.message); return this.validators.filter((validator) => !validator.isValid(value)).map((validator) => validator);
} }
} }

View File

@@ -56,4 +56,24 @@ describe('CardViewFloatItemModel', () => {
expect(itemModel.isValid('42.3')).toBe(true, 'For "42.3" it should be true'); expect(itemModel.isValid('42.3')).toBe(true, 'For "42.3" it should be true');
expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false'); expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false');
}); });
it('should validate based on defined constraints', () => {
const constrainedProperties = {
label: 'Tribe',
value: '42.42',
key: 'tribe',
dataType: 'd:float',
constraints: [{
id: 'constraint-id',
type: 'MINMAX',
parameters: { minValue: 10, maxValue: 40 }
}]
};
const itemModel = new CardViewFloatItemModel(constrainedProperties);
expect(itemModel.isValid(itemModel.value)).toBe(false, '42.42 is bigger than maximum allowed');
itemModel.value = '9.1';
expect(itemModel.isValid(itemModel.value)).toBe(false, '9.1 is less than minimum allowed');
});
}); });

View File

@@ -56,4 +56,24 @@ describe('CardViewIntItemModel', () => {
expect(itemModel.isValid('42.3')).toBe(false, 'For "42.3" it should be false'); expect(itemModel.isValid('42.3')).toBe(false, 'For "42.3" it should be false');
expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false'); expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false');
}); });
it('should validate based on defined constraints', () => {
const constrainedProperties = {
label: 'Tribe',
value: '20',
key: 'tribe',
dataType: 'd:float',
constraints: [{
id: 'constraint-id',
type: 'MINMAX',
parameters: { minValue: 10, maxValue: 15 }
}]
};
const itemModel = new CardViewIntItemModel(constrainedProperties);
expect(itemModel.isValid(itemModel.value)).toBe(false, '20 is bigger than maximum allowed');
itemModel.value = '5';
expect(itemModel.isValid(itemModel.value)).toBe(false, '5 is less than minimum allowed');
});
}); });

View File

@@ -85,4 +85,24 @@ describe('CardViewTextItemModel', () => {
expect(itemModel.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3'); expect(itemModel.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3');
}); });
}); });
it('should validate based on defined constraints', () => {
const constrainedProperties = {
label: 'Tribe',
value: 'test',
key: 'tribe',
dataType: 'd:text',
constraints: [{
id: 'constraint-id',
type: 'REGEX',
parameters: { expression: '^(?=.*test).*' }
}]
};
const itemModel = new CardViewTextItemModel(constrainedProperties);
expect(itemModel.isValid(itemModel.value)).toBe(true);
itemModel.value = 'dummy';
expect(itemModel.isValid(itemModel.value)).toBe(false, '`dummy` is not a constraint expression pattern');
});
}); });

View File

@@ -0,0 +1,34 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CardViewItemValidator } from '../interfaces/card-view.interfaces';
export interface LengthValidatorParams {
minLength: number;
maxLength: number;
}
export class CardViewItemLengthValidator implements CardViewItemValidator {
message = 'CORE.CARDVIEW.VALIDATORS.LENGTH_VALIDATION_ERROR';
constructor(private minLength: number, private maxLength: number) {}
isValid(value: string = ''): boolean {
const stringLength = value.length;
return stringLength >= this.minLength && stringLength <= this.maxLength;
}
}

View File

@@ -0,0 +1,34 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CardViewItemValidator } from '../interfaces/card-view.interfaces';
export interface MatchValidatorParams {
expression: string;
flags?: string;
}
export class CardViewItemMatchValidator implements CardViewItemValidator {
message = 'CORE.CARDVIEW.VALIDATORS.MATCH_VALIDATION_ERROR';
constructor(private expression: string, private flags?: string) {}
isValid(value: string): boolean {
const regex = new RegExp(this.expression, this.flags);
return value === '' || regex.test(value);
}
}

View File

@@ -0,0 +1,37 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CardViewItemValidator } from '../interfaces/card-view.interfaces';
import { CardViewItemIntValidator } from './card-view-item-int.validator';
export interface MinMaxValidatorParams {
minValue: number;
maxValue: number;
}
export class CardViewItemMinMaxValidator implements CardViewItemValidator {
message = 'CORE.CARDVIEW.VALIDATORS.MINMAX_VALIDATION_ERROR';
private intValidator: CardViewItemIntValidator;
constructor(private minValue: number, private maxValue: number) {
this.intValidator = new CardViewItemIntValidator();
}
isValid(value: number): boolean {
return this.intValidator.isValid(value) && (value >= this.minValue && value <= this.maxValue);
}
}

View File

@@ -17,3 +17,7 @@
export * from './card-view-item-int.validator'; export * from './card-view-item-int.validator';
export * from './card-view-item-float.validator'; export * from './card-view-item-float.validator';
export * from './card-view-item-match.valiator';
export * from './card-view-item-minmax.valiator';
export * from './card-view-item-length.valiator';
export * from './validators.map';

View File

@@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CardViewItemMatchValidator, MatchValidatorParams } from './card-view-item-match.valiator';
import { CardViewItemMinMaxValidator, MinMaxValidatorParams } from './card-view-item-minmax.valiator';
import { CardViewItemLengthValidator, LengthValidatorParams } from './card-view-item-length.valiator';
const validators = {
minmax: (parameters: MinMaxValidatorParams) => new CardViewItemMinMaxValidator(parameters.minValue, parameters.maxValue),
regex: (parameters: MatchValidatorParams) => new CardViewItemMatchValidator(parameters.expression),
length: (parameters: LengthValidatorParams) => new CardViewItemLengthValidator(parameters.minLength, parameters.maxLength)
};
export default validators;

View File

@@ -175,7 +175,10 @@
"NONE": "None", "NONE": "None",
"VALIDATORS": { "VALIDATORS": {
"FLOAT_VALIDATION_ERROR": "Use a number format", "FLOAT_VALIDATION_ERROR": "Use a number format",
"INT_VALIDATION_ERROR": "Use an integer format" "INT_VALIDATION_ERROR": "Use an integer format",
"LENGTH_VALIDATION_ERROR": "Value should be minimum {{ minLength }} and maximum {{ maxLength }} in length",
"MATCH_VALIDATION_ERROR": "Value doesn't match pattern: {{ expression }}",
"MINMAX_VALIDATION_ERROR": "Value should be between minimum {{ minValue }} and maximum {{ maxValue }}"
}, },
"MORE": "More" "MORE": "More"
}, },