From b4f27d15b8d261ba82ad7198a175c21a896a1ac5 Mon Sep 17 00:00:00 2001 From: Tomasz Gnyp <49343696+tomgny@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:02:49 +0100 Subject: [PATCH] AAE-20848 Add display external property widget (#9429) * AAE-20848 Add display external property widget * revert css changes * AAE-20848 validate validatable types * AAE-20848 implement suggestions * fix lint --- cspell.json | 3 +- .../widgets/core/form-field-types.ts | 9 + .../widgets/core/form-field-validator.ts | 3 +- .../widgets/core/form-field.model.spec.ts | 52 +++++ .../widgets/core/form-field.model.ts | 8 +- lib/core/src/lib/i18n/en.json | 1 + .../cloud-form-rendering.service.ts | 4 +- .../display-external-property.widget.html | 30 +++ .../display-external-property.widget.scss | 9 + .../display-external-property.widget.spec.ts | 187 ++++++++++++++++++ .../display-external-property.widget.ts | 109 ++++++++++ 11 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts diff --git a/cspell.json b/cspell.json index 98f0ddb238..f71962e5a1 100644 --- a/cspell.json +++ b/cspell.json @@ -143,7 +143,8 @@ "xsrf", "BPMECM", "berseria", - "zestiria" + "zestiria", + "validatable" ], "dictionaries": [ "html", 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 d42dde283b..14b77c89da 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 @@ -48,6 +48,7 @@ export class FormFieldTypes { static DISPLAY_RICH_TEXT: string = 'display-rich-text'; static JSON: string = 'json'; static DATA_TABLE: string = 'data-table'; + static DISPLAY_EXTERNAL_PROPERTY: string = 'display-external-property'; static READONLY_TYPES: string[] = [ FormFieldTypes.HYPERLINK, @@ -56,10 +57,18 @@ export class FormFieldTypes { FormFieldTypes.GROUP ]; + static VALIDATABLE_TYPES: string[] = [ + FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY + ]; + static isReadOnlyType(type: string) { return FormFieldTypes.READONLY_TYPES.includes(type); } + static isValidatableType(type: string) { + return FormFieldTypes.VALIDATABLE_TYPES.includes(type); + } + static isContainerType(type: string) { return type === FormFieldTypes.CONTAINER || type === FormFieldTypes.GROUP; } 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 47ad8c43bc..5c69630546 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 @@ -49,7 +49,8 @@ export class RequiredFieldValidator implements FormFieldValidator { FormFieldTypes.DATE, FormFieldTypes.DATETIME, FormFieldTypes.ATTACH_FOLDER, - FormFieldTypes.DECIMAL + FormFieldTypes.DECIMAL, + FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY ]; isSupported(field: FormFieldModel): boolean { diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts index eb82b7bd39..d04ba3a01d 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts @@ -17,6 +17,7 @@ import { DateFnsUtils } from '../../../../common'; import { FormFieldTypes } from './form-field-types'; +import { RequiredFieldValidator } from './form-field-validator'; import { FormFieldModel } from './form-field.model'; import { FormModel } from './form.model'; @@ -881,4 +882,55 @@ describe('FormFieldModel', () => { }); }); + + it('should validate readOnly field if it is validatable', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'mockDisplayExternalPropertyFieldId', + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + readOnly: true, + required: true, + value: null + }); + + const validator = new RequiredFieldValidator(); + form.fieldValidators = [validator]; + + expect(FormFieldTypes.isValidatableType(FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY)).toBeTrue(); + expect(field.validate()).toBe(false); + }); + + it('should validate NOT readOnly field if it is validatable', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'mockDisplayExternalPropertyFieldId', + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + readOnly: false, + required: true, + value: null + }); + + const validator = new RequiredFieldValidator(); + form.fieldValidators = [validator]; + + expect(FormFieldTypes.isValidatableType(FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY)).toBeTrue(); + expect(field.validate()).toBe(false); + }); + + it('should NOT validate readOnly field if it is NOT validatable', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'mockTextFieldId', + type: FormFieldTypes.TEXT, + readOnly: true, + required: true, + value: null + }); + + const validator = new RequiredFieldValidator(); + form.fieldValidators = [validator]; + + expect(FormFieldTypes.isValidatableType(FormFieldTypes.TEXT)).toBeFalse(); + expect(field.validate()).toBe(true); + }); }); 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 6e3f153e6b..e49b79c9f9 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 @@ -86,6 +86,7 @@ export class FormFieldModel extends FormWidgetModel { leftLabels: boolean = false; variableConfig: VariableConfig; schemaDefinition: DataColumn[]; + externalProperty?: string; // container model members numberOfColumns: number = 1; @@ -143,7 +144,7 @@ export class FormFieldModel extends FormWidgetModel { validate(): boolean { this.validationSummary = new ErrorMessageModel(); - if (!this.readOnly) { + if (this.isFieldValidatable()) { const validators = this.form.fieldValidators || []; for (const validator of validators) { if (!validator.validate(this)) { @@ -156,6 +157,10 @@ export class FormFieldModel extends FormWidgetModel { return this._isValid; } + private isFieldValidatable(): boolean { + return !this.readOnly || FormFieldTypes.isValidatableType(this.type); + } + constructor(form: any, json?: any) { super(form, json); if (json) { @@ -204,6 +209,7 @@ export class FormFieldModel extends FormWidgetModel { this.variableConfig = json.variableConfig; this.schemaDefinition = json.schemaDefinition; this.precision = json.precision; + this.externalProperty = json.externalProperty; if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') { this.placeholder = json.placeholder; diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index ddf46e0d1b..31a7400960 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -48,6 +48,7 @@ "REST_API_FAILED": "The server `{{ hostname }}` is not reachable", "VARIABLE_DROPDOWN_OPTIONS_FAILED": "There was a problem loading dropdown elements. Please contact administrator.", "DATA_TABLE_LOAD_FAILED": "There was a problem loading table elements. Please contact administrator.", + "EXTERNAL_PROPERTY_LOAD_FAILED": "There was a problem loading external property. Please contact administrator.", "FILE_NAME": "File Name", "NO_FILE_ATTACHED": "No file attached", "VALIDATOR": { diff --git a/lib/process-services-cloud/src/lib/form/components/cloud-form-rendering.service.ts b/lib/process-services-cloud/src/lib/form/components/cloud-form-rendering.service.ts index 1d6a692953..d7b2204a2a 100644 --- a/lib/process-services-cloud/src/lib/form/components/cloud-form-rendering.service.ts +++ b/lib/process-services-cloud/src/lib/form/components/cloud-form-rendering.service.ts @@ -27,6 +27,7 @@ import { RadioButtonsCloudWidgetComponent } from './widgets/radio-buttons/radio- import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget'; import { DisplayRichTextWidgetComponent } from './widgets/display-rich-text/display-rich-text.widget'; import { DataTableWidgetComponent } from './widgets/data-table/data-table.widget'; +import { DisplayExternalPropertyWidgetComponent } from './widgets/display-external-property/display-external-property.widget'; @Injectable({ providedIn: 'root' @@ -45,7 +46,8 @@ export class CloudFormRenderingService extends FormRenderingService { [FormFieldTypes.RADIO_BUTTONS]: () => RadioButtonsCloudWidgetComponent, [FormFieldTypes.ALFRESCO_FILE_VIEWER]: () => FileViewerWidgetComponent, [FormFieldTypes.DISPLAY_RICH_TEXT]: () => DisplayRichTextWidgetComponent, - [FormFieldTypes.DATA_TABLE]: () => DataTableWidgetComponent + [FormFieldTypes.DATA_TABLE]: () => DataTableWidgetComponent, + [FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY]: () => DisplayExternalPropertyWidgetComponent }, true); } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html new file mode 100644 index 0000000000..91e0948605 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html @@ -0,0 +1,30 @@ +
+
+ +
+ +
+ + + + + + + + +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss new file mode 100644 index 0000000000..87383c2475 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss @@ -0,0 +1,9 @@ +.adf { + &-display-external-property-widget { + width: 100%; + + .adf-label { + top: 20px; + } + } +} diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts new file mode 100644 index 0000000000..80bbc2276f --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts @@ -0,0 +1,187 @@ +/*! + * @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 { FormService, FormFieldModel, FormModel, FormFieldTypes, LogService } from '@alfresco/adf-core'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { DisplayExternalPropertyWidgetComponent } from './display-external-property.widget'; +import { FormCloudService } from '../../../services/form-cloud.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('DisplayExternalPropertyWidgetComponent', () => { + let loader: HarnessLoader; + let widget: DisplayExternalPropertyWidgetComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let logService: LogService; + let logServiceSpy: jasmine.Spy; + let formCloudService: FormCloudService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + NoopAnimationsModule, + ReactiveFormsModule, + DisplayExternalPropertyWidgetComponent + ], + providers: [FormService] + }).compileComponents(); + + fixture = TestBed.createComponent(DisplayExternalPropertyWidgetComponent); + widget = fixture.componentInstance; + element = fixture.nativeElement; + loader = TestbedHarnessEnvironment.loader(fixture); + logService = TestBed.inject(LogService); + formCloudService = TestBed.inject(FormCloudService); + + logServiceSpy = spyOn(logService, 'error'); + }); + + it('should display initial value', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + readOnly: true, + externalProperty: 'fruitName', + value: 'banana' + }); + + fixture.detectChanges(); + + const input = await loader.getHarness(MatInputHarness); + expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy(); + expect(await input.getValue()).toBe('banana'); + }); + + describe('when property load fails', () => { + beforeEach(() => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + externalProperty: 'fruitName', + value: null + }); + + fixture.detectChanges(); + }); + + it('should display the error message', () => { + const errorElement = element.querySelector('error-widget'); + expect(errorElement.textContent.trim()).toContain('FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED'); + }); + + it('should log the error', () => { + expect(logServiceSpy).toHaveBeenCalledWith('External property not found'); + }); + }); + + describe('when property is in preview state', () => { + beforeEach(() => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + externalProperty: true, + value: null + }); + + spyOn(formCloudService, 'getPreviewState').and.returnValue(true); + fixture.detectChanges(); + }); + + it('should NOT display the error message', () => { + const errorElement = element.querySelector('error-widget'); + expect(errorElement).toBeFalsy(); + }); + + it('should NOT log the error', () => { + expect(logServiceSpy).not.toHaveBeenCalled(); + }); + }); + + describe('when is required', () => { + beforeEach(() => { + widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY, + required: true + }); + + fixture.detectChanges(); + }); + + it('should be able to display label with asterisk', () => { + 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: 'external-property-id', + name: 'external-property-name', + value: '', + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY + }); + + fixture.detectChanges(); + + 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', () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: false }), { + id: 'external-property-id', + name: 'external-property-name', + value: '', + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY + }); + + fixture.detectChanges(); + + 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', () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), { + id: 'external-property-id', + name: 'external-property-name', + value: '', + type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY + }); + + fixture.detectChanges(); + + 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/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts new file mode 100644 index 0000000000..d937763271 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts @@ -0,0 +1,109 @@ +/*! + * @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 { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { + WidgetComponent, + FormService, + LogService, + FormBaseModule +} from '@alfresco/adf-core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormCloudService } from '../../../services/form-cloud.service'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +@Component({ + standalone: true, + imports: [ + CommonModule, + TranslateModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + FormBaseModule + ], + selector: 'adf-cloud-display-external-property', + templateUrl: './display-external-property.widget.html', + styleUrls: ['./display-external-property.widget.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, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DisplayExternalPropertyWidgetComponent extends WidgetComponent implements OnInit { + + propertyLoadFailed = false; + previewState = false; + propertyControl: FormControl; + + constructor( + public readonly formService: FormService, + private readonly formCloudService: FormCloudService, + private readonly logService: LogService + ) { + super(formService); + } + + ngOnInit(): void { + this.initFormControl(); + this.initPreviewState(); + this.handleFailedPropertyLoad(); + } + + private initFormControl(): void { + this.propertyControl = new FormControl( + { + value: this.field?.value, + disabled: this.field?.readOnly || this.readOnly + }, + this.isRequired() ? [Validators.required] : [] + ); + } + + private isPropertyLoadFailed(): boolean { + return this.field.externalProperty && !this.field.value; + } + + private handleFailedPropertyLoad(): void { + if (this.isPropertyLoadFailed()) { + this.handleError('External property not found'); + } + } + + private initPreviewState(): void { + this.previewState = this.formCloudService.getPreviewState(); + } + + private handleError(error: any): void { + if (!this.previewState) { + this.propertyLoadFailed = true; + this.logService.error(error); + } + } +}