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);
+ }
+ }
+}