From 16005ef298da538764c3847d14f74533b9065ffc Mon Sep 17 00:00:00 2001 From: Bartosz Sekula Date: Thu, 15 Feb 2024 14:23:55 +0100 Subject: [PATCH] Create a new widget for decimal type (#9335) * [AAE-19481] Create a new widget for decimal type * update * CR * unit for decimal widgets * added test for positibe validator * added decimal validator tests * cr --- .../card-view-item-dispatcher.component.ts | 7 +- ...card-view-textitem-properties.interface.ts | 8 + .../models/card-view-baseitem.model.ts | 3 +- .../models/card-view-floatitem.model.ts | 1 + .../models/card-view-intitem.model.ts | 17 +- ...w-item-only-positive-int.validator.spec.ts | 50 +++++ ...d-view-item-only-positive-int.validator.ts | 40 ++++ .../validators/card-view.validators.ts | 1 + .../widgets/core/form-field-types.ts | 1 + .../widgets/core/form-field-validator.spec.ts | 72 ++++++- .../widgets/core/form-field-validator.ts | 52 +++++- .../widgets/core/form-field.model.ts | 12 +- .../widgets/decimal/decimal.component.html | 44 +++++ .../widgets/decimal/decimal.component.scss | 9 + .../widgets/decimal/decimal.component.spec.ts | 175 ++++++++++++++++++ .../widgets/decimal/decimal.component.ts | 49 +++++ .../src/lib/form/components/widgets/index.ts | 3 + .../form/services/form-rendering.service.ts | 1 + lib/core/src/lib/i18n/en.json | 5 +- 19 files changed, 533 insertions(+), 17 deletions(-) create mode 100644 lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.spec.ts create mode 100644 lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.ts create mode 100644 lib/core/src/lib/form/components/widgets/decimal/decimal.component.html create mode 100644 lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss create mode 100644 lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts create mode 100644 lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts diff --git a/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts b/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts index cfa5e2898e..c121fa9b89 100644 --- a/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts @@ -91,10 +91,9 @@ export class CardViewItemDispatcherComponent implements OnChanges { this.loaded = true; } - Object.keys(changes) - .map((changeName) => [changeName, changes[changeName]]) - .forEach(([inputParamName, simpleChange]: [string, SimpleChange]) => { - this.componentReference.instance[inputParamName] = simpleChange.currentValue; + Object.entries(changes) + .forEach(([changeName, change]: [string, SimpleChange]) => { + this.componentReference.instance[changeName] = change.currentValue; }); this.proxy('ngOnChanges', changes); diff --git a/lib/core/src/lib/card-view/interfaces/card-view-textitem-properties.interface.ts b/lib/core/src/lib/card-view/interfaces/card-view-textitem-properties.interface.ts index a2432ea9f6..92fbfcdd6c 100644 --- a/lib/core/src/lib/card-view/interfaces/card-view-textitem-properties.interface.ts +++ b/lib/core/src/lib/card-view/interfaces/card-view-textitem-properties.interface.ts @@ -24,3 +24,11 @@ export interface CardViewTextItemProperties extends CardViewItemProperties { pipes?: CardViewTextItemPipeProperty[]; clickCallBack?: any; } + +export interface CardViewIntItemProperties extends CardViewTextItemProperties { + allowOnlyPositiveNumbers?: boolean; +} + +export interface CardViewFloatItemProperties extends CardViewTextItemProperties { + precision?: number; +} diff --git a/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts b/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts index 46042f665c..456cf73e86 100644 --- a/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts @@ -48,7 +48,8 @@ export abstract class CardViewBaseItemModel { if (props?.constraints?.length ?? 0) { for (const constraint of props.constraints) { if (constraint.type !== 'LIST') { - this.validators.push(validatorsMap[constraint.type.toLowerCase()](constraint.parameters)); + const validatorFactory = validatorsMap[constraint.type.toLowerCase()]; + this.validators.push(validatorFactory(constraint.parameters)); } } } diff --git a/lib/core/src/lib/card-view/models/card-view-floatitem.model.ts b/lib/core/src/lib/card-view/models/card-view-floatitem.model.ts index a63cd78a77..b3ddd268a6 100644 --- a/lib/core/src/lib/card-view/models/card-view-floatitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-floatitem.model.ts @@ -29,6 +29,7 @@ export class CardViewFloatItemModel extends CardViewTextItemModel implements Car super(cardViewTextItemProperties); this.validators.push(new CardViewItemFloatValidator()); + if (cardViewTextItemProperties.value && !cardViewTextItemProperties.multivalued) { this.value = parseFloat(cardViewTextItemProperties.value); } diff --git a/lib/core/src/lib/card-view/models/card-view-intitem.model.ts b/lib/core/src/lib/card-view/models/card-view-intitem.model.ts index 66ca02e467..74dc84eeaf 100644 --- a/lib/core/src/lib/card-view/models/card-view-intitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-intitem.model.ts @@ -18,19 +18,24 @@ import { CardViewItem } from '../interfaces/card-view-item.interface'; import { DynamicComponentModel } from '../../common/services/dynamic-component-mapper.service'; import { CardViewTextItemModel } from './card-view-textitem.model'; -import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; -import { CardViewItemIntValidator } from '../validators/card-view.validators'; +import { CardViewIntItemProperties } from '../interfaces/card-view.interfaces'; +import { CardViewItemIntValidator, CardViewItemPositiveIntValidator } from '../validators/card-view.validators'; export class CardViewIntItemModel extends CardViewTextItemModel implements CardViewItem, DynamicComponentModel { type: string = 'int'; inputType: string = 'number'; - constructor(cardViewTextItemProperties: CardViewTextItemProperties) { - super(cardViewTextItemProperties); + constructor(cardViewIntItemProperties: CardViewIntItemProperties) { + super(cardViewIntItemProperties); this.validators.push(new CardViewItemIntValidator()); - if (cardViewTextItemProperties.value && !cardViewTextItemProperties.multivalued) { - this.value = parseInt(cardViewTextItemProperties.value, 10); + + if (cardViewIntItemProperties.allowOnlyPositiveNumbers) { + this.validators.push(new CardViewItemPositiveIntValidator()); + } + + if (cardViewIntItemProperties.value && !cardViewIntItemProperties.multivalued) { + this.value = parseInt(cardViewIntItemProperties.value, 10); } } diff --git a/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.spec.ts b/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.spec.ts new file mode 100644 index 0000000000..45f244727e --- /dev/null +++ b/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.spec.ts @@ -0,0 +1,50 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { CardViewItemPositiveIntValidator } from './card-view-item-only-positive-int.validator'; + +describe('CardViewItemPositiveIntValidator', () => { + let validator: CardViewItemPositiveIntValidator; + + beforeEach(() => { + validator = new CardViewItemPositiveIntValidator(); + }); + + it('should return false for invalid integer value', () => { + expect(validator.isValid('a')).toBeFalse(); + }); + + it('should return false for negative value', () => { + expect(validator.isValid(-1)).toBeFalse(); + }); + + it('should return true for positive value', () => { + expect(validator.isValid(1)).toBeTrue(); + }); + + it('should return true for empty value', () => { + expect(validator.isValid('')).toBeTrue(); + }); + + it('should work for negative string value', () => { + expect(validator.isValid('-1')).toBeFalse(); + }); + + it('should work for positive string value', () => { + expect(validator.isValid('1')).toBeTrue(); + }); +}); diff --git a/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.ts b/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.ts new file mode 100644 index 0000000000..83905b8b2c --- /dev/null +++ b/lib/core/src/lib/card-view/validators/card-view-item-only-positive-int.validator.ts @@ -0,0 +1,40 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 class CardViewItemPositiveIntValidator implements CardViewItemValidator { + message = 'CORE.CARDVIEW.VALIDATORS.ONLY_POSITIVE_NUMBER'; + + isValid(value: any | any[]): boolean { + if (Array.isArray(value)) { + return value.every(this.isPositiveNumber); + } + + const valueIsNotSet = value === ''; + + return valueIsNotSet || + ( + !isNaN(value) && + this.isPositiveNumber(value) + ); + } + + private isPositiveNumber(value: any): boolean { + return parseInt(value, 10) >= 0; + } +} diff --git a/lib/core/src/lib/card-view/validators/card-view.validators.ts b/lib/core/src/lib/card-view/validators/card-view.validators.ts index 4935531241..b8a5407cd9 100644 --- a/lib/core/src/lib/card-view/validators/card-view.validators.ts +++ b/lib/core/src/lib/card-view/validators/card-view.validators.ts @@ -16,6 +16,7 @@ */ export * from './card-view-item-int.validator'; +export * from './card-view-item-only-positive-int.validator'; export * from './card-view-item-float.validator'; export * from './card-view-item-match.valiator'; export * from './card-view-item-minmax.valiator'; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts index 2af8a706c6..d42dde283b 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts @@ -24,6 +24,7 @@ export class FormFieldTypes { static TEXT: string = 'text'; static STRING: string = 'string'; static INTEGER: string = 'integer'; + static DECIMAL: string = 'bigdecimal'; static MULTILINE_TEXT: string = 'multi-line-text'; static DROPDOWN: string = 'dropdown'; static HYPERLINK: string = 'hyperlink'; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts index 50e6aebcb1..6ae72e13ff 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts @@ -31,7 +31,8 @@ import { MinDateTimeFieldValidator, MaxDateFieldValidator, MinDateFieldValidator, - DateTimeFieldValidator + DateTimeFieldValidator, + DecimalFieldValidator } from './form-field-validator'; import { FormFieldModel } from './form-field.model'; import { FormModel } from './form.model'; @@ -1117,4 +1118,73 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBeFalse(); }); }); + + describe('DecimalFieldValidator', () => { + let decimalValidator: DecimalFieldValidator; + + beforeEach(() => { + decimalValidator = new DecimalFieldValidator(); + }); + + it('should validate decimal with correct precision', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: 1.22, + precision: 2 + }); + + expect(decimalValidator.validate(field)).toBeTrue(); + }); + + it('should return true when value is of lower precision', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: 1.2, + precision: 2 + }); + + expect(decimalValidator.validate(field)).toBeTrue(); + }); + + it('should return false when value is of higher precision', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: 1.22, + precision: 1 + }); + + expect(decimalValidator.validate(field)).toBeFalse(); + }); + + it('should validate decimal of wrong precision when value is of type string', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: '1.22', + precision: 1 + }); + + expect(decimalValidator.validate(field)).toBeFalse(); + }); + + it('should return false, when value is a negative number and of correct precission', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: -1.22, + precision: 1 + }); + + expect(decimalValidator.validate(field)).toBeFalse(); + }); + + it('should return true, when value is a positive number and of correct precission', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DECIMAL, + value: -1.22, + precision: 3 + }); + + expect(decimalValidator.validate(field)).toBeTrue(); + }); + + }); }); diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts index 574eca70b8..47ad8c43bc 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts @@ -48,7 +48,8 @@ export class RequiredFieldValidator implements FormFieldValidator { FormFieldTypes.DYNAMIC_TABLE, FormFieldTypes.DATE, FormFieldTypes.DATETIME, - FormFieldTypes.ATTACH_FOLDER + FormFieldTypes.ATTACH_FOLDER, + FormFieldTypes.DECIMAL ]; isSupported(field: FormFieldModel): boolean { @@ -431,6 +432,7 @@ export class MinValueFieldValidator implements FormFieldValidator { private supportedTypes = [ FormFieldTypes.NUMBER, + FormFieldTypes.DECIMAL, FormFieldTypes.AMOUNT ]; @@ -461,6 +463,7 @@ export class MaxValueFieldValidator implements FormFieldValidator { private supportedTypes = [ FormFieldTypes.NUMBER, + FormFieldTypes.DECIMAL, FormFieldTypes.AMOUNT ]; @@ -553,6 +556,50 @@ export class FixedValueFieldValidator implements FormFieldValidator { } } +export class DecimalFieldValidator implements FormFieldValidator { + + private supportedTypes = [ + FormFieldTypes.DECIMAL + ]; + + isSupported(field: FormFieldModel): boolean { + return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.value; + } + + validate(field: FormFieldModel): boolean { + const shouldValidateField = this.isSupported(field) && field.isVisible; + + if (!shouldValidateField) { + return true; + } + + const precision = field.precision; + const fieldValue = field.value; + + if (!isNumberValue(fieldValue)) { + field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DECIMAL_NUMBER'; + return false; + } + + const value = typeof fieldValue === 'string' ? fieldValue : fieldValue.toString(); + const valueParts = value.split('.'); + const decimalPart = valueParts[1]; + + if (decimalPart === undefined) { + return true; + } + + if (decimalPart.length > precision) { + field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DECIMAL_PRECISION'; + field.validationSummary.attributes.set('precision', precision.toString()); + + return false; + } + + return true; + } +} + export const FORM_FIELD_VALIDATORS = [ new RequiredFieldValidator(), new NumberFieldValidator(), @@ -567,5 +614,6 @@ export const FORM_FIELD_VALIDATORS = [ new MaxDateFieldValidator(), new FixedValueFieldValidator(), new MinDateTimeFieldValidator(), - new MaxDateTimeFieldValidator() + new MaxDateTimeFieldValidator(), + new DecimalFieldValidator() ]; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index 118e7ea069..6e3f153e6b 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -58,6 +58,7 @@ export class FormFieldModel extends FormWidgetModel { maxValue: string; maxDateRangeValue: number = 0; minDateRangeValue: number = 0; + precision: number; dynamicDateRangeSelection: boolean; regexPattern: string; options: FormFieldOption[] = []; @@ -100,8 +101,10 @@ export class FormFieldModel extends FormWidgetModel { } set value(v: any) { - this._value = v; - this.updateForm(); + if (v !== this._value) { + this._value = v; + this.updateForm(); + } } get readOnly(): boolean { @@ -200,6 +203,7 @@ export class FormFieldModel extends FormWidgetModel { this.groupsRestriction = json.groupsRestriction?.groups; this.variableConfig = json.variableConfig; this.schemaDefinition = json.schemaDefinition; + this.precision = json.precision; if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') { this.placeholder = json.placeholder; @@ -459,6 +463,10 @@ export class FormFieldModel extends FormWidgetModel { this.form.values[this.id] = this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10); break; } + case FormFieldTypes.DECIMAL: { + this.form.values[this.id] = parseFloat(this.value); + break; + }; case FormFieldTypes.BOOLEAN: { this.form.values[this.id] = this.value !== null && this.value !== undefined ? this.value : false; break; diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html new file mode 100644 index 0000000000..d712aadb51 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html @@ -0,0 +1,44 @@ +
+
+ +
+ +
+ + + + + + + + + +
+
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss new file mode 100644 index 0000000000..863f8aafb6 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss @@ -0,0 +1,9 @@ +.adf { + &-decimal-widget { + width: 100%; + + .adf-label { + top: 20px; + } + } +} diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts new file mode 100644 index 0000000000..b1f91c2df8 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts @@ -0,0 +1,175 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { HarnessLoader } from '@angular/cdk/testing'; + +import { DecimalWidgetComponent } from './decimal.component'; +import { FormService } from '../../../services/form.service'; +import { FormFieldModel, FormFieldTypes, FormModel } from '../core'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { MatTooltipHarness } from '@angular/material/tooltip/testing'; +import { MatInputModule } from '@angular/material/input'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreTestingModule } from '../../../../testing'; +import { FormsModule } from '@angular/forms'; + +describe('DecimalComponent', () => { + let loader: HarnessLoader; + let widget: DecimalWidgetComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CoreTestingModule, + MatInputModule, + FormsModule + ], + declarations: [DecimalWidgetComponent], + providers: [FormService] + }).compileComponents(); + + fixture = TestBed.createComponent(DecimalWidgetComponent); + widget = fixture.componentInstance; + element = fixture.nativeElement; + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + describe('when tooltip is set', () => { + beforeEach(() => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DECIMAL, + tooltip: 'my custom tooltip' + }); + fixture.detectChanges(); + }); + + it('should show tooltip', async () => { + const input = await loader.getHarness(MatInputHarness); + await (await input.host()).hover(); + + const tooltip = await loader.getHarness(MatTooltipHarness); + expect(await tooltip.getTooltipText()).toBe('my custom tooltip'); + }); + + it('should hide tooltip', async () => { + const input = await loader.getHarness(MatInputHarness); + await (await input.host()).hover(); + await (await input.host()).mouseAway(); + + const tooltip = await loader.getHarness(MatTooltipHarness); + expect(await tooltip.isOpen()).toBe(false); + }); + }); + + describe('when is required', () => { + beforeEach(() => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DECIMAL, + required: true + }); + + fixture.detectChanges(); + }); + + it('should be marked as invalid after interaction', async () => { + const input = await loader.getHarness(MatInputHarness); + expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy(); + + const inputHost = await input.host(); + await inputHost.blur(); + fixture.detectChanges(); + + const invalidElement = fixture.nativeElement.querySelector('.adf-invalid'); + + expect(invalidElement).toBeTruthy(); + }); + + it('should be able to display label with asterisk', async () => { + const asterisk = element.querySelector('.adf-asterisk'); + + expect(asterisk).toBeTruthy(); + expect(asterisk?.textContent).toEqual('*'); + }); + }); + + describe('when form model has left labels', () => { + it('should have left labels classes on leftLabels true', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), { + id: 'decimal-id', + name: 'decimal-name', + value: '', + type: FormFieldTypes.DECIMAL, + readOnly: false, + required: true + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + const widgetContainer = element.querySelector('.adf-left-label-input-container'); + expect(widgetContainer).not.toBeNull(); + + const adfLeftLabel = element.querySelector('.adf-left-label'); + expect(adfLeftLabel).not.toBeNull(); + }); + + it('should not have left labels classes on leftLabels false', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: false }), { + id: 'decimal-id', + name: 'decimal-name', + value: '', + type: FormFieldTypes.DECIMAL, + readOnly: false, + required: true + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + const widgetContainer = element.querySelector('.adf-left-label-input-container'); + expect(widgetContainer).toBeNull(); + + const adfLeftLabel = element.querySelector('.adf-left-label'); + expect(adfLeftLabel).toBeNull(); + }); + + it('should not have left labels classes on leftLabels not present', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), { + id: 'decimal-id', + name: 'decimal-name', + value: '', + type: FormFieldTypes.DECIMAL, + readOnly: false, + required: true + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + const widgetContainer = element.querySelector('.adf-left-label-input-container'); + expect(widgetContainer).toBeNull(); + + const adfLeftLabel = element.querySelector('.adf-left-label'); + expect(adfLeftLabel).toBeNull(); + }); + }); +}); diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts new file mode 100644 index 0000000000..2d88fd5be0 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts @@ -0,0 +1,49 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { FormService } from '../../../services/form.service'; +import { WidgetComponent } from '../widget.component'; + +@Component({ + selector: 'adf-decimal', + templateUrl: './decimal.component.html', + styleUrls: ['./decimal.component.scss'], + host: { + '(click)': 'event($event)', + '(blur)': 'event($event)', + '(change)': 'event($event)', + '(focus)': 'event($event)', + '(focusin)': 'event($event)', + '(focusout)': 'event($event)', + '(input)': 'event($event)', + '(invalid)': 'event($event)', + '(select)': 'event($event)' + }, + encapsulation: ViewEncapsulation.None +}) +export class DecimalWidgetComponent extends WidgetComponent implements OnInit { + displayValue: number; + + constructor(public formService: FormService) { + super(formService); + } + + ngOnInit(): void { + this.displayValue = this.field.value; + } +} diff --git a/lib/core/src/lib/form/components/widgets/index.ts b/lib/core/src/lib/form/components/widgets/index.ts index d3b77d6fa2..bdec205e7b 100644 --- a/lib/core/src/lib/form/components/widgets/index.ts +++ b/lib/core/src/lib/form/components/widgets/index.ts @@ -30,6 +30,7 @@ import { TextWidgetComponent } from './text/text.widget'; import { DateTimeWidgetComponent } from './date-time/date-time.widget'; import { JsonWidgetComponent } from './json/json.widget'; import { BaseViewerWidgetComponent } from './base-viewer/base-viewer.widget'; +import { DecimalWidgetComponent } from './decimal/decimal.component'; // core export * from './widget.component'; @@ -39,6 +40,7 @@ export * from './core'; export * from './unknown/unknown.widget'; export * from './text/text.widget'; export * from './number/number.widget'; +export * from './decimal/decimal.component'; export * from './checkbox/checkbox.widget'; export * from './multiline-text/multiline-text.widget'; export * from './hyperlink/hyperlink.widget'; @@ -55,6 +57,7 @@ export const WIDGET_DIRECTIVES: any[] = [ UnknownWidgetComponent, TextWidgetComponent, NumberWidgetComponent, + DecimalWidgetComponent, CheckboxWidgetComponent, MultilineTextWidgetComponentComponent, HyperlinkWidgetComponent, diff --git a/lib/core/src/lib/form/services/form-rendering.service.ts b/lib/core/src/lib/form/services/form-rendering.service.ts index f8f05737d6..9b786d27d1 100644 --- a/lib/core/src/lib/form/services/form-rendering.service.ts +++ b/lib/core/src/lib/form/services/form-rendering.service.ts @@ -35,6 +35,7 @@ export class FormRenderingService extends DynamicComponentMapper { [FormFieldTypes.TEXT]: DynamicComponentResolver.fromType(widgets.TextWidgetComponent), [FormFieldTypes.STRING]: DynamicComponentResolver.fromType(widgets.TextWidgetComponent), [FormFieldTypes.INTEGER]: DynamicComponentResolver.fromType(widgets.NumberWidgetComponent), + [FormFieldTypes.DECIMAL]: DynamicComponentResolver.fromType(widgets.DecimalWidgetComponent), [FormFieldTypes.MULTILINE_TEXT]: DynamicComponentResolver.fromType(widgets.MultilineTextWidgetComponentComponent), [FormFieldTypes.BOOLEAN]: DynamicComponentResolver.fromType(widgets.CheckboxWidgetComponent), [FormFieldTypes.DATE]: DynamicComponentResolver.fromType(widgets.DateWidgetComponent), diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index 44b6a40f20..ddf46e0d1b 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -52,6 +52,8 @@ "NO_FILE_ATTACHED": "No file attached", "VALIDATOR": { "INVALID_NUMBER": "Use a different number format", + "INVALID_DECIMAL_NUMBER": "Use a decimal number format", + "INVALID_DECIMAL_PRECISION": "Incorrect decimal value, there should be a maximum of {{precision}} digits after the decimal point.", "INVALID_DATE": "Use a different date format", "INVALID_VALUE": "Enter a different value", "NOT_GREATER_THAN": "Can't be greater than {{ maxValue }}", @@ -192,7 +194,8 @@ "INT_VALIDATION_ERROR": "Use an integer format", "LENGTH_VALIDATION_ERROR": "Value should be between {{ minLength }} and {{ maxLength }} in length", "MATCH_VALIDATION_ERROR": "Value doesn't match pattern: {{ expression }}", - "MINMAX_VALIDATION_ERROR": "Value should be between {{ minValue }} and {{ maxValue }}" + "MINMAX_VALIDATION_ERROR": "Value should be between {{ minValue }} and {{ maxValue }}", + "ONLY_POSITIVE_NUMBER": "Only positive value is allowed" }, "MORE": "More" },