mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
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
This commit is contained in:
@@ -143,7 +143,8 @@
|
||||
"xsrf",
|
||||
"BPMECM",
|
||||
"berseria",
|
||||
"zestiria"
|
||||
"zestiria",
|
||||
"validatable"
|
||||
],
|
||||
"dictionaries": [
|
||||
"html",
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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": {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<div
|
||||
class="adf-textfield adf-display-external-property-widget {{ field.className }}"
|
||||
[class.adf-invalid]="!field.isValid && isTouched()"
|
||||
[class.adf-readonly]="field.readOnly"
|
||||
[class.adf-left-label-input-container]="field.leftLabels"
|
||||
>
|
||||
<div *ngIf="field.leftLabels">
|
||||
<label class="adf-label adf-left-label" [attr.for]="field.id">
|
||||
{{ field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-form-field [hideRequiredMarker]="true">
|
||||
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">
|
||||
{{ field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span>
|
||||
</label>
|
||||
<input matInput
|
||||
class="adf-input"
|
||||
type="text"
|
||||
data-automation-id="adf-display-external-property-widget"
|
||||
[id]="field.id"
|
||||
[formControl]="propertyControl"
|
||||
>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container *ngIf="!previewState">
|
||||
<error-widget *ngIf="propertyLoadFailed" [required]="'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate"></error-widget>
|
||||
</ng-container>
|
||||
</div>
|
@@ -0,0 +1,9 @@
|
||||
.adf {
|
||||
&-display-external-property-widget {
|
||||
width: 100%;
|
||||
|
||||
.adf-label {
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<DisplayExternalPropertyWidgetComponent>;
|
||||
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: '<id>' }), {
|
||||
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: '<id>' }), {
|
||||
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: '<id>' }), {
|
||||
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: '<id>' }), {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user