mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-46246 Format typed values in form display widgets v2 (#11952)
* AAE-46246 Format typed values in form display widgets
This commit is contained in:
+24
-2
@@ -15,10 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, inject, AfterViewInit, DestroyRef, InjectionToken } from '@angular/core';
|
import { ChangeDetectorRef, Component, inject, AfterViewInit, OnInit, DestroyRef, InjectionToken } from '@angular/core';
|
||||||
import { debounceTime, filter, isObservable, Observable } from 'rxjs';
|
import { debounceTime, filter, isObservable, Observable } from 'rxjs';
|
||||||
import { FormRulesEvent } from '../../../events';
|
import { FormRulesEvent } from '../../../events';
|
||||||
import { FormExpressionService } from '../../../services/form-expression.service';
|
import { FormExpressionService } from '../../../services/form-expression.service';
|
||||||
|
import { FormFieldValueFormatterService } from '../../../services/form-field-value-formatter.service';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||||
import { WidgetComponent } from '../widget.component';
|
import { WidgetComponent } from '../widget.component';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@@ -33,11 +35,14 @@ export const ADF_DISPLAY_TEXT_SETTINGS = new InjectionToken<DisplayTextWidgetSet
|
|||||||
template: '',
|
template: '',
|
||||||
standalone: true
|
standalone: true
|
||||||
})
|
})
|
||||||
export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent implements AfterViewInit {
|
export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent implements OnInit, AfterViewInit {
|
||||||
private readonly formExpressionService = inject(FormExpressionService);
|
private readonly formExpressionService = inject(FormExpressionService);
|
||||||
private readonly cdr = inject(ChangeDetectorRef);
|
private readonly cdr = inject(ChangeDetectorRef);
|
||||||
private enableExpressionEvaluation: boolean = false;
|
private enableExpressionEvaluation: boolean = false;
|
||||||
protected originalFieldValue?: string;
|
protected originalFieldValue?: string;
|
||||||
|
protected readonly formatter = inject(FormFieldValueFormatterService);
|
||||||
|
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
|
||||||
|
protected formattingEnabled = false;
|
||||||
|
|
||||||
private readonly settings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
private readonly settings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
||||||
optional: true
|
optional: true
|
||||||
@@ -45,6 +50,13 @@ export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent imp
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
if (isObservable(this.formattingEnabledToken)) {
|
||||||
|
this.formattingEnabledToken.pipe(takeUntilDestroyed()).subscribe((enabled: boolean) => {
|
||||||
|
this.formattingEnabled = enabled ?? false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formattingEnabled = this.formattingEnabledToken ?? false;
|
||||||
|
}
|
||||||
if (isObservable(this.settings)) {
|
if (isObservable(this.settings)) {
|
||||||
this.settings.pipe(takeUntilDestroyed()).subscribe((data: DisplayTextWidgetSettings) => {
|
this.settings.pipe(takeUntilDestroyed()).subscribe((data: DisplayTextWidgetSettings) => {
|
||||||
this.updateSettingsBasedProperties(data);
|
this.updateSettingsBasedProperties(data);
|
||||||
@@ -54,6 +66,16 @@ export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent imp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const value = this.field?.value;
|
||||||
|
const shouldFormatValue =
|
||||||
|
this.formattingEnabled && value != null && typeof value !== 'string' && this.formatter.hasFormatter(this.field?.type ?? '');
|
||||||
|
|
||||||
|
if (shouldFormatValue) {
|
||||||
|
this.field.value = this.formatter.format(this.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override ngAfterViewInit() {
|
override ngAfterViewInit() {
|
||||||
if (this.enableExpressionEvaluation) {
|
if (this.enableExpressionEvaluation) {
|
||||||
this.storeOriginalValue();
|
this.storeOriginalValue();
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2025 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, DestroyRef, inject, OnInit } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { isObservable } from 'rxjs';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { FormRulesEvent } from '../../../events/form-rules.event';
|
||||||
|
import { ADF_CUSTOM_MESSAGE } from './custom-validation-message.token';
|
||||||
|
import { WidgetComponent } from '../widget.component';
|
||||||
|
import { FormFieldValueFormatterService } from '../../../services/form-field-value-formatter.service';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export abstract class FormattableTextWidgetComponent extends WidgetComponent implements OnInit {
|
||||||
|
protected readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly enableCustomMessage = inject(ADF_CUSTOM_MESSAGE, { optional: true });
|
||||||
|
private readonly formatter = inject(FormFieldValueFormatterService);
|
||||||
|
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
|
||||||
|
private formattingEnabled = false;
|
||||||
|
displayValue = '';
|
||||||
|
|
||||||
|
onValueChange(value: string): void {
|
||||||
|
this.field.value = value;
|
||||||
|
this.displayValue = this.computeDisplayValue();
|
||||||
|
this.onFieldChanged(this.field);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected computeDisplayValue(): string {
|
||||||
|
const value = this.field.value;
|
||||||
|
const isReadOnly = this.field.readOnly || this.readOnly;
|
||||||
|
if (this.formattingEnabled && isReadOnly && value != null && typeof value !== 'string' && this.formatter.hasFormatter(this.field.type)) {
|
||||||
|
return this.formatter.format(this.field);
|
||||||
|
}
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initValueFormatting();
|
||||||
|
this.initCustomValidationMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initValueFormatting(): void {
|
||||||
|
if (isObservable(this.formattingEnabledToken)) {
|
||||||
|
this.formattingEnabledToken.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||||
|
this.formattingEnabled = enabled ?? false;
|
||||||
|
this.displayValue = this.computeDisplayValue();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formattingEnabled = this.formattingEnabledToken ?? false;
|
||||||
|
this.displayValue = this.computeDisplayValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formService.formRulesEvent
|
||||||
|
.pipe(
|
||||||
|
filter((event: FormRulesEvent) => event?.type === 'fieldValueChanged'),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.displayValue = this.computeDisplayValue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initCustomValidationMessage(): void {
|
||||||
|
if (this.enableCustomMessage != null) {
|
||||||
|
if (isObservable(this.enableCustomMessage)) {
|
||||||
|
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||||
|
if (this.field) {
|
||||||
|
this.field.enableCustomValidationMessage = enabled ?? false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.field.enableCustomValidationMessage = this.enableCustomMessage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.field.enableCustomValidationMessage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,10 @@
|
|||||||
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { FormFieldModel, FormModel } from '../core';
|
import { FormFieldModel, FormModel } from '../core';
|
||||||
|
import { FormFieldTypes } from '../core/form-field-types';
|
||||||
import { DisplayTextWidgetComponent } from './display-text.widget';
|
import { DisplayTextWidgetComponent } from './display-text.widget';
|
||||||
import { ADF_DISPLAY_TEXT_SETTINGS } from '../base-display-text/base-display-text.widget';
|
import { ADF_DISPLAY_TEXT_SETTINGS } from '../base-display-text/base-display-text.widget';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||||
import { FormService } from '../../../services/form.service';
|
import { FormService } from '../../../services/form.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
@@ -240,4 +242,117 @@ describe('DisplayTextWidgetComponent', () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('typed value formatting', () => {
|
||||||
|
describe('when flag is on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DisplayTextWidgetComponent],
|
||||||
|
providers: [
|
||||||
|
FormService,
|
||||||
|
{ provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } },
|
||||||
|
{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format a direct People value to full name', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'f1',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alyssa', lastName: 'Adcock' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.field.value).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format a direct Group value to group name', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'f1',
|
||||||
|
type: FormFieldTypes.FUNCTIONAL_GROUP,
|
||||||
|
value: [{ id: 'g1', name: 'Engineering' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.field.value).toBe('Engineering');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not contain [object Object] for a complex value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'f1',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alice', lastName: 'Brown' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(String(widget.field.value)).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve expression templates for People via FormExpressionService', () => {
|
||||||
|
const form = new FormModel({
|
||||||
|
fields: [
|
||||||
|
{ id: 'displayText1', type: 'display-text', value: 'Selected: ${field.peopleField}' },
|
||||||
|
{ id: 'peopleField', type: FormFieldTypes.PEOPLE, value: [{ firstName: 'Alyssa', lastName: 'Adcock' }] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.field = form.getFieldById('displayText1');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.field.value).toBe('Selected: Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-evaluate expression with formatted value when dependent People field changes', (done) => {
|
||||||
|
const form = new FormModel({
|
||||||
|
fields: [
|
||||||
|
{ id: 'displayText1', type: 'display-text', value: 'Selected: ${field.peopleField}' },
|
||||||
|
{ id: 'peopleField', type: FormFieldTypes.PEOPLE, value: [{ firstName: 'Alyssa', lastName: 'Adcock' }] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
formService = TestBed.inject(FormService);
|
||||||
|
|
||||||
|
widget.field = form.getFieldById('displayText1');
|
||||||
|
const peopleField = form.getFieldById('peopleField');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.field.value).toBe('Selected: Alyssa Adcock');
|
||||||
|
|
||||||
|
peopleField.value = [{ firstName: 'Jane', lastName: 'Smith' }];
|
||||||
|
formService.formRulesEvent.next({ type: 'fieldValueChanged', field: peopleField } as any);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(widget.field.value).toBe('Selected: Jane Smith');
|
||||||
|
done();
|
||||||
|
}, 350);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag is off', () => {
|
||||||
|
it('should not format a complex field value (default behaviour preserved)', () => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DisplayTextWidgetComponent],
|
||||||
|
providers: [FormService]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
|
||||||
|
const rawValue = [{ firstName: 'Alyssa', lastName: 'Adcock' }];
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'f1',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: rawValue
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger only ngOnInit (no full render — raw array would crash TranslatePipe without the flag)
|
||||||
|
widget.ngOnInit();
|
||||||
|
|
||||||
|
expect(widget.field.value).toEqual(rawValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,13 @@ export class DisplayTextWidgetComponent extends BaseDisplayTextWidgetComponent {
|
|||||||
|
|
||||||
protected evaluateExpressions(): void {
|
protected evaluateExpressions(): void {
|
||||||
if (this.field) {
|
if (this.field) {
|
||||||
this.field.value = this.resolveExpressions(this.field.value);
|
const value = this.field.value;
|
||||||
|
const isFormattableValue = value != null && typeof value !== 'string';
|
||||||
|
if (this.formattingEnabled && isFormattableValue && this.formatter.hasFormatter(this.field.type)) {
|
||||||
|
this.field.value = this.formatter.format(this.field);
|
||||||
|
} else {
|
||||||
|
this.field.value = this.resolveExpressions(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
[id]="field.id"
|
[id]="field.id"
|
||||||
[required]="field.required"
|
[required]="field.required"
|
||||||
[(ngModel)]="field.value"
|
[ngModel]="displayValue"
|
||||||
(ngModelChange)="onFieldChanged(field)"
|
(ngModelChange)="onValueChange($event)"
|
||||||
[disabled]="field.readOnly || readOnly"
|
[disabled]="field.readOnly || readOnly"
|
||||||
[placeholder]="field.placeholder"
|
[placeholder]="field.placeholder"
|
||||||
[title]="field.tooltip"
|
[title]="field.tooltip"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</textarea>
|
</textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div *ngIf="field.maxLength > 0" class="adf-multiline-word-counter">
|
<div *ngIf="field.maxLength > 0" class="adf-multiline-word-counter">
|
||||||
<span class="adf-multiline-word-counter-value">{{ field?.value?.length || 0 }}/{{ field.maxLength }}</span>
|
<span class="adf-multiline-word-counter-value">{{ displayValue?.length || 0 }}/{{ field.maxLength }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="adf-error-messages-container">
|
<div class="adf-error-messages-container">
|
||||||
<error-widget [error]="field.validationSummary" />
|
<error-widget [error]="field.validationSummary" />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { MultilineTextWidgetComponentComponent } from './multiline-text.widget';
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||||
import { of, Subject } from 'rxjs';
|
import { of, Subject } from 'rxjs';
|
||||||
|
|
||||||
describe('MultilineTextWidgetComponentComponent', () => {
|
describe('MultilineTextWidgetComponentComponent', () => {
|
||||||
@@ -298,4 +299,92 @@ describe('MultilineTextWidgetComponentComponent - ADF_CUSTOM_MESSAGE', () => {
|
|||||||
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
|
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('typed value formatting', () => {
|
||||||
|
describe('when flag is on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [MultilineTextWidgetComponentComponent],
|
||||||
|
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return formatted name for a People value in read-only mode', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alice', lastName: 'Brown' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('Alice Brown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return [object Object] for a complex field value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alice', lastName: 'Brown' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(String(widget.displayValue)).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through plain string values unchanged', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'multiline-id',
|
||||||
|
type: FormFieldTypes.MULTILINE_TEXT,
|
||||||
|
value: 'plain text',
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('plain text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not JSON-stringify a Date value for an unregistered type', () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'date-id',
|
||||||
|
type: FormFieldTypes.MULTILINE_TEXT,
|
||||||
|
value: date,
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(String(widget.displayValue)).toBe(String(date));
|
||||||
|
expect(String(widget.displayValue)).not.toContain('"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag is off', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [MultilineTextWidgetComponentComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not format complex field values (default behaviour preserved)', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alice', lastName: 'Brown' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).not.toBe('Alice Brown');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,16 +18,13 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
/* eslint-disable @angular-eslint/component-selector */
|
||||||
|
|
||||||
import { NgIf } from '@angular/common';
|
import { NgIf } from '@angular/common';
|
||||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, ViewEncapsulation } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { isObservable } from 'rxjs';
|
|
||||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
|
||||||
import { ErrorWidgetComponent } from '../error/error.component';
|
import { ErrorWidgetComponent } from '../error/error.component';
|
||||||
import { WidgetComponent } from '../widget.component';
|
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'multiline-text-widget',
|
selector: 'multiline-text-widget',
|
||||||
@@ -47,23 +44,4 @@ import { WidgetComponent } from '../widget.component';
|
|||||||
imports: [MatFormFieldModule, NgIf, TranslatePipe, MatInputModule, FormsModule, ErrorWidgetComponent],
|
imports: [MatFormFieldModule, NgIf, TranslatePipe, MatInputModule, FormsModule, ErrorWidgetComponent],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class MultilineTextWidgetComponentComponent extends WidgetComponent implements OnInit {
|
export class MultilineTextWidgetComponentComponent extends FormattableTextWidgetComponent {}
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
|
||||||
private readonly enableCustomMessage = inject(ADF_CUSTOM_MESSAGE, { optional: true });
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (this.enableCustomMessage != null) {
|
|
||||||
if (isObservable(this.enableCustomMessage)) {
|
|
||||||
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.enableCustomValidationMessage = enabled ?? false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.field.enableCustomValidationMessage = this.enableCustomMessage;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.field.enableCustomValidationMessage = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,9 +18,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
[id]="field.id"
|
[id]="field.id"
|
||||||
[required]="field.required"
|
[required]="field.required"
|
||||||
[value]="field.value"
|
[ngModel]="displayValue"
|
||||||
[(ngModel)]="field.value"
|
(ngModelChange)="onValueChange($event)"
|
||||||
(ngModelChange)="onFieldChanged(field)"
|
|
||||||
[disabled]="field.readOnly || readOnly"
|
[disabled]="field.readOnly || readOnly"
|
||||||
[textMask]="{mask: mask, isReversed: isMaskReversed}"
|
[textMask]="{mask: mask, isReversed: isMaskReversed}"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
|
|||||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||||
import { of, Subject } from 'rxjs';
|
import { of, Subject } from 'rxjs';
|
||||||
|
|
||||||
describe('TextWidgetComponent', () => {
|
describe('TextWidgetComponent', () => {
|
||||||
@@ -698,4 +699,143 @@ describe('TextWidgetComponent - ADF_CUSTOM_MESSAGE', () => {
|
|||||||
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
|
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('typed value formatting', () => {
|
||||||
|
describe('when flag is on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TextWidgetComponent],
|
||||||
|
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(TextWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return formatted name for a People value in read-only mode', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alyssa', lastName: 'Adcock' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return comma-separated labels for a multi-select dropdown in read-only mode', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: [
|
||||||
|
{ id: 'a', name: 'Apple' },
|
||||||
|
{ id: 'b', name: 'Banana' }
|
||||||
|
],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('Apple, Banana');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return [object Object] for a complex field value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: { firstName: 'Alice', lastName: 'Brown' },
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(String(widget.displayValue)).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through plain string values unchanged', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'text-id',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
value: 'hello',
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not JSON-stringify a Date value for an unregistered type', () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'date-id',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
value: date,
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(String(widget.displayValue)).toBe(String(date));
|
||||||
|
expect(String(widget.displayValue)).not.toContain('"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not format a value for an unregistered type', () => {
|
||||||
|
const value = { foo: 'bar' };
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'object-id',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
value,
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe(value as unknown as string);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag is off', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TextWidgetComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(TextWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not format complex field values (default behaviour preserved)', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alyssa', lastName: 'Adcock' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).not.toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag emits via observable', () => {
|
||||||
|
it('should format value after observable emits true', () => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TextWidgetComponent],
|
||||||
|
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: of(true) }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(TextWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||||
|
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'people-field',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: [{ firstName: 'Alyssa', lastName: 'Adcock' }],
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.displayValue).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,16 +18,14 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
/* eslint-disable @angular-eslint/component-selector */
|
||||||
|
|
||||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||||
import { Component, DestroyRef, Directive, inject, InjectionToken, Input, OnInit, TemplateRef, ViewEncapsulation } from '@angular/core';
|
import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { isObservable } from 'rxjs';
|
|
||||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
|
||||||
import { ErrorWidgetComponent } from '../error/error.component';
|
import { ErrorWidgetComponent } from '../error/error.component';
|
||||||
import { WidgetComponent } from '../widget.component';
|
import { WidgetComponent } from '../widget.component';
|
||||||
|
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
||||||
import { InputMaskDirective } from './text-mask.component';
|
import { InputMaskDirective } from './text-mask.component';
|
||||||
|
|
||||||
type FieldStatusTemplate = TemplateRef<{ $implicit: WidgetComponent }>;
|
type FieldStatusTemplate = TemplateRef<{ $implicit: WidgetComponent }>;
|
||||||
@@ -66,29 +64,14 @@ export class FieldStatusTemplateDirective {
|
|||||||
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, InputMaskDirective, NgTemplateOutlet],
|
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, InputMaskDirective, NgTemplateOutlet],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class TextWidgetComponent extends WidgetComponent implements OnInit {
|
export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||||
mask: string;
|
mask: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
isMaskReversed: boolean;
|
isMaskReversed: boolean;
|
||||||
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
|
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
|
||||||
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
override ngOnInit() {
|
||||||
private readonly enableCustomMessage = inject(ADF_CUSTOM_MESSAGE, { optional: true });
|
super.ngOnInit();
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
if (this.enableCustomMessage != null) {
|
|
||||||
if (isObservable(this.enableCustomMessage)) {
|
|
||||||
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.enableCustomValidationMessage = enabled ?? false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.field.enableCustomValidationMessage = this.enableCustomMessage;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.field.enableCustomValidationMessage = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.params) {
|
if (this.field.params) {
|
||||||
this.mask = this.field.params['inputMask'];
|
this.mask = this.field.params['inputMask'];
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export * from './services/form.service';
|
|||||||
export * from './services/form-expression.service';
|
export * from './services/form-expression.service';
|
||||||
export * from './services/form-validation-service.interface';
|
export * from './services/form-validation-service.interface';
|
||||||
export * from './services/widget-visibility.service';
|
export * from './services/widget-visibility.service';
|
||||||
|
export * from './services/form-field-value-formatter.service';
|
||||||
|
export * from './services/form-field-value-formatter.token';
|
||||||
|
|
||||||
export * from './pipes';
|
export * from './pipes';
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { FormExpressionService } from './form-expression.service';
|
import { FormExpressionService } from './form-expression.service';
|
||||||
import { FormModel } from '../components/widgets/core';
|
import { FormModel } from '../components/widgets/core';
|
||||||
|
import { FormFieldValueFormatterService } from './form-field-value-formatter.service';
|
||||||
|
import { FormFieldTypes } from '../components/widgets/core/form-field-types';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from './form-field-value-formatter.token';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
describe('FormExpressionService', () => {
|
describe('FormExpressionService', () => {
|
||||||
let service: FormExpressionService;
|
let service: FormExpressionService;
|
||||||
@@ -269,6 +273,83 @@ describe('FormExpressionService', () => {
|
|||||||
expect(result).toBe('true');
|
expect(result).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when ADF_TYPED_VALUE_FORMATTING_ENABLED token is provided as true', () => {
|
||||||
|
let formattingService: FormExpressionService;
|
||||||
|
let formatter: FormFieldValueFormatterService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [FormExpressionService, { provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: of(true) }]
|
||||||
|
});
|
||||||
|
formattingService = TestBed.inject(FormExpressionService);
|
||||||
|
formatter = TestBed.inject(FormFieldValueFormatterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format object field values via the registered formatter', () => {
|
||||||
|
const mockField = {
|
||||||
|
id: 'peopleField',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: { firstName: 'Alyssa', lastName: 'Adcock' }
|
||||||
|
};
|
||||||
|
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||||
|
spyOn(formatter, 'hasFormatter').and.returnValue(true);
|
||||||
|
spyOn(formatter, 'formatValue').and.returnValue('Alyssa Adcock');
|
||||||
|
|
||||||
|
const result = formattingService.resolveExpressions(formModel, '${field.peopleField}');
|
||||||
|
|
||||||
|
expect(result).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to JSON.stringify when no formatter is registered', () => {
|
||||||
|
const mockField = {
|
||||||
|
id: 'objectField',
|
||||||
|
type: 'unregistered-type',
|
||||||
|
value: { a: 1 }
|
||||||
|
};
|
||||||
|
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||||
|
spyOn(formatter, 'hasFormatter').and.returnValue(false);
|
||||||
|
|
||||||
|
const result = formattingService.resolveExpressions(formModel, '${field.objectField}');
|
||||||
|
|
||||||
|
expect(result).toBe('{"a":1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a Date result as a native string instead of quoted ISO', () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
const mockField = {
|
||||||
|
id: 'datetimeField',
|
||||||
|
type: FormFieldTypes.DATETIME,
|
||||||
|
value: date
|
||||||
|
};
|
||||||
|
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||||
|
spyOn(formatter, 'hasFormatter').and.returnValue(false);
|
||||||
|
|
||||||
|
const result = formattingService.resolveExpressions(formModel, '${field.datetimeField}');
|
||||||
|
|
||||||
|
expect(result).toBe(String(date));
|
||||||
|
expect(result).not.toContain('"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when ADF_TYPED_VALUE_FORMATTING_ENABLED token is not provided', () => {
|
||||||
|
it('should default to JSON.stringify even if a formatter exists', () => {
|
||||||
|
const formatter = TestBed.inject(FormFieldValueFormatterService);
|
||||||
|
const mockField = {
|
||||||
|
id: 'peopleField',
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
value: { firstName: 'Alyssa', lastName: 'Adcock' }
|
||||||
|
};
|
||||||
|
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||||
|
const hasFormatterSpy = spyOn(formatter, 'hasFormatter');
|
||||||
|
|
||||||
|
const result = service.resolveExpressions(formModel, '${field.peopleField}');
|
||||||
|
|
||||||
|
expect(hasFormatterSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('{"firstName":"Alyssa","lastName":"Adcock"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when escapeHtml is true', () => {
|
describe('when escapeHtml is true', () => {
|
||||||
let getFieldSpy: jasmine.Spy;
|
let getFieldSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { isObservable } from 'rxjs';
|
||||||
import { FormModel } from '../components/widgets/core';
|
import { FormModel } from '../components/widgets/core';
|
||||||
|
import { FormFieldValueFormatterService } from './form-field-value-formatter.service';
|
||||||
|
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from './form-field-value-formatter.token';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -27,6 +31,20 @@ export class FormExpressionService {
|
|||||||
private readonly VARIABLE_PREFIX = 'variable.';
|
private readonly VARIABLE_PREFIX = 'variable.';
|
||||||
private readonly VARIABLES_REGEX = /(?:field|variable)\.[a-zA-Z_$][a-zA-Z0-9_$]*/g;
|
private readonly VARIABLES_REGEX = /(?:field|variable)\.[a-zA-Z_$][a-zA-Z0-9_$]*/g;
|
||||||
|
|
||||||
|
private readonly formFieldValueFormatter = inject(FormFieldValueFormatterService);
|
||||||
|
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
|
||||||
|
private formattingEnabled = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (isObservable(this.formattingEnabledToken)) {
|
||||||
|
this.formattingEnabledToken.pipe(takeUntilDestroyed()).subscribe((enabled: boolean) => {
|
||||||
|
this.formattingEnabled = enabled ?? false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formattingEnabled = this.formattingEnabledToken ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resolveExpressions(form: FormModel, formField: string, escapeHtml?: boolean): string {
|
resolveExpressions(form: FormModel, formField: string, escapeHtml?: boolean): string {
|
||||||
let result = formField || '';
|
let result = formField || '';
|
||||||
|
|
||||||
@@ -37,24 +55,10 @@ export class FormExpressionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
let expressionResult = this.resolveExpression(form, match);
|
const rawResult = this.resolveExpression(form, match);
|
||||||
if (expressionResult === null || expressionResult === undefined) {
|
let expressionResult = this.normalizeExpressionResult(form, match, rawResult);
|
||||||
expressionResult = '';
|
|
||||||
} else if (typeof expressionResult !== 'string') {
|
|
||||||
expressionResult = JSON.stringify(expressionResult);
|
|
||||||
}
|
|
||||||
if (escapeHtml) {
|
if (escapeHtml) {
|
||||||
expressionResult = expressionResult
|
expressionResult = this.escapeHtmlEntities(expressionResult);
|
||||||
.split('&')
|
|
||||||
.join('&')
|
|
||||||
.split('<')
|
|
||||||
.join('<')
|
|
||||||
.split('>')
|
|
||||||
.join('>')
|
|
||||||
.split('"')
|
|
||||||
.join('"')
|
|
||||||
.split("'")
|
|
||||||
.join(''');
|
|
||||||
}
|
}
|
||||||
result = result.replace(match, expressionResult);
|
result = result.replace(match, expressionResult);
|
||||||
}
|
}
|
||||||
@@ -62,6 +66,37 @@ export class FormExpressionService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeExpressionResult(form: FormModel, match: string, expressionResult: any): string {
|
||||||
|
if (expressionResult == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expressionResult === 'string') {
|
||||||
|
return expressionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatTypedExpressionResult(form, match, expressionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatTypedExpressionResult(form: FormModel, match: string, expressionResult: any): string {
|
||||||
|
if (!this.formattingEnabled) {
|
||||||
|
return JSON.stringify(expressionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldId = this.extractFieldIdFromMatch(match);
|
||||||
|
const sourceField = fieldId ? form.getFieldById(fieldId) : undefined;
|
||||||
|
|
||||||
|
if (sourceField && this.formFieldValueFormatter.hasFormatter(sourceField.type)) {
|
||||||
|
return this.formFieldValueFormatter.formatValue(expressionResult, sourceField);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formFieldValueFormatter.stringifyValue(expressionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private escapeHtmlEntities(value: string): string {
|
||||||
|
return value.split('&').join('&').split('<').join('<').split('>').join('>').split('"').join('"').split("'").join(''');
|
||||||
|
}
|
||||||
|
|
||||||
private resolveExpression(form: FormModel, expression: any): any {
|
private resolveExpression(form: FormModel, expression: any): any {
|
||||||
if (expression === undefined || expression === null) {
|
if (expression === undefined || expression === null) {
|
||||||
return expression;
|
return expression;
|
||||||
@@ -96,6 +131,14 @@ export class FormExpressionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractFieldIdFromMatch(match: string): string | null {
|
||||||
|
const inner = match.slice(2, -1).trim();
|
||||||
|
if (inner.startsWith(this.FIELD_PREFIX)) {
|
||||||
|
return inner.slice(this.FIELD_PREFIX.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getFieldDependencies(expression: string): string[] {
|
getFieldDependencies(expression: string): string[] {
|
||||||
const dependencies: string[] = [];
|
const dependencies: string[] = [];
|
||||||
const matches = expression.match(this.GLOBAL_EXPRESSION_REGEX);
|
const matches = expression.match(this.GLOBAL_EXPRESSION_REGEX);
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2025 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 { TestBed } from '@angular/core/testing';
|
||||||
|
import { FormFieldValueFormatterService } from './form-field-value-formatter.service';
|
||||||
|
import { FormFieldModel } from '../components/widgets/core/form-field.model';
|
||||||
|
import { FormFieldTypes } from '../components/widgets/core/form-field-types';
|
||||||
|
import { FormModel } from '../components/widgets/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test helper that creates a FormFieldModel attached to a fresh FormModel for unit tests.
|
||||||
|
*
|
||||||
|
* @param type form field type identifier (e.g., FormFieldTypes.PEOPLE)
|
||||||
|
* @param value initial field value
|
||||||
|
* @param options optional dropdown/radio option list
|
||||||
|
* @returns a fully-wired FormFieldModel
|
||||||
|
*/
|
||||||
|
function makeField(type: string, value: any, options?: { id: string; name: string }[]): FormFieldModel {
|
||||||
|
const form = new FormModel();
|
||||||
|
const field = new FormFieldModel(form, { id: 'f1', type, value });
|
||||||
|
if (options) {
|
||||||
|
field.options = options;
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FormFieldValueFormatterService', () => {
|
||||||
|
let service: FormFieldValueFormatterService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(FormFieldValueFormatterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasFormatter', () => {
|
||||||
|
it('should return true for registered types', () => {
|
||||||
|
expect(service.hasFormatter(FormFieldTypes.PEOPLE)).toBeTrue();
|
||||||
|
expect(service.hasFormatter(FormFieldTypes.FUNCTIONAL_GROUP)).toBeTrue();
|
||||||
|
expect(service.hasFormatter(FormFieldTypes.DROPDOWN)).toBeTrue();
|
||||||
|
expect(service.hasFormatter(FormFieldTypes.RADIO_BUTTONS)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unregistered types', () => {
|
||||||
|
expect(service.hasFormatter('unknown-type')).toBeFalse();
|
||||||
|
expect(service.hasFormatter('text')).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatValue - base cases', () => {
|
||||||
|
it('should return empty string for null', () => {
|
||||||
|
const field = makeField('text', null);
|
||||||
|
expect(service.formatValue(null, field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for undefined', () => {
|
||||||
|
const field = makeField('text', undefined);
|
||||||
|
expect(service.formatValue(undefined, field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through string values unchanged', () => {
|
||||||
|
const field = makeField('text', 'hello');
|
||||||
|
expect(service.formatValue('hello', field)).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should JSON.stringify unknown object types', () => {
|
||||||
|
const field = makeField('text', { foo: 'bar' });
|
||||||
|
expect(service.formatValue({ foo: 'bar' }, field)).toBe('{"foo":"bar"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Date values using native string instead of JSON', () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
const field = makeField('datetime', date);
|
||||||
|
expect(service.formatValue(date, field)).toBe(String(date));
|
||||||
|
expect(service.formatValue(date, field)).not.toContain('"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render numeric values without JSON quoting', () => {
|
||||||
|
const field = makeField('integer', 42);
|
||||||
|
expect(service.formatValue(42, field)).toBe('42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render boolean values without JSON quoting', () => {
|
||||||
|
const field = makeField('boolean', true);
|
||||||
|
expect(service.formatValue(true, field)).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stringifyValue', () => {
|
||||||
|
it('should return empty string for null and undefined', () => {
|
||||||
|
expect(service.stringifyValue(null)).toBe('');
|
||||||
|
expect(service.stringifyValue(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through strings unchanged', () => {
|
||||||
|
expect(service.stringifyValue('hello')).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Date using native string, not ISO JSON', () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
expect(service.stringifyValue(date)).toBe(String(date));
|
||||||
|
expect(service.stringifyValue(date)).not.toBe(JSON.stringify(date));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render primitives without JSON quoting', () => {
|
||||||
|
expect(service.stringifyValue(42)).toBe('42');
|
||||||
|
expect(service.stringifyValue(true)).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should JSON.stringify plain objects and arrays', () => {
|
||||||
|
expect(service.stringifyValue({ foo: 'bar' })).toBe('{"foo":"bar"}');
|
||||||
|
expect(service.stringifyValue([{ id: 'a' }])).toBe('[{"id":"a"}]');
|
||||||
|
expect(service.stringifyValue({ foo: 'bar' })).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('People formatter', () => {
|
||||||
|
it('should return empty string for null', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, null);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for empty array', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, []);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format a single user object (single-select mode)', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, { firstName: 'Alyssa', lastName: 'Adcock' });
|
||||||
|
expect(service.format(field)).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format an array with one user', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, [{ firstName: 'Alyssa', lastName: 'Adcock' }]);
|
||||||
|
expect(service.format(field)).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format multiple users separated by comma', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, [
|
||||||
|
{ firstName: 'Alice', lastName: 'Brown' },
|
||||||
|
{ firstName: 'Bob', lastName: 'Smith' }
|
||||||
|
]);
|
||||||
|
expect(service.format(field)).toBe('Alice Brown, Bob Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to username when no first/last name', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, [{ username: 'jdoe' }]);
|
||||||
|
expect(service.format(field)).toBe('jdoe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to email when no first/last/username', () => {
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, [{ email: 'a@b.com' }]);
|
||||||
|
expect(service.format(field)).toBe('a@b.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Group formatter', () => {
|
||||||
|
it('should return empty string for null', () => {
|
||||||
|
const field = makeField(FormFieldTypes.FUNCTIONAL_GROUP, null);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for empty array', () => {
|
||||||
|
const field = makeField(FormFieldTypes.FUNCTIONAL_GROUP, []);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format a single group object', () => {
|
||||||
|
const field = makeField(FormFieldTypes.FUNCTIONAL_GROUP, { id: 'g1', name: 'Engineering' });
|
||||||
|
expect(service.format(field)).toBe('Engineering');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format an array of groups', () => {
|
||||||
|
const field = makeField(FormFieldTypes.FUNCTIONAL_GROUP, [{ name: 'Eng' }, { name: 'QA' }]);
|
||||||
|
expect(service.format(field)).toBe('Eng, QA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip groups with no name', () => {
|
||||||
|
const field = makeField(FormFieldTypes.FUNCTIONAL_GROUP, [{ id: 'g1' }, { name: 'QA' }]);
|
||||||
|
expect(service.format(field)).toBe('QA');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dropdown formatter', () => {
|
||||||
|
const options = [
|
||||||
|
{ id: 'a', name: 'Apple' },
|
||||||
|
{ id: 'b', name: 'Banana' }
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return empty string for null', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, null, options);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for empty string', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, '', options);
|
||||||
|
expect(service.formatValue('', field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape A: should look up label for string id', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, 'a', options);
|
||||||
|
expect(service.format(field)).toBe('Apple');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape A fallback: should return the id when not found in options', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, 'unknown', options);
|
||||||
|
expect(service.format(field)).toBe('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape B: should use .name from object value', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, { id: 'a', name: 'Apple' }, options);
|
||||||
|
expect(service.format(field)).toBe('Apple');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape B no name: should JSON.stringify', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, { id: 'a' }, options);
|
||||||
|
expect(service.format(field)).toBe('{"id":"a"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape C: should format array of objects', () => {
|
||||||
|
const field = makeField(
|
||||||
|
FormFieldTypes.DROPDOWN,
|
||||||
|
[
|
||||||
|
{ id: 'a', name: 'Apple' },
|
||||||
|
{ id: 'b', name: 'Banana' }
|
||||||
|
],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
expect(service.format(field)).toBe('Apple, Banana');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape C string array: should look up labels for each id', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, ['a', 'b'], options);
|
||||||
|
expect(service.format(field)).toBe('Apple, Banana');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape C empty array: should return empty string', () => {
|
||||||
|
const field = makeField(FormFieldTypes.DROPDOWN, [], options);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Radio formatter', () => {
|
||||||
|
const options = [
|
||||||
|
{ id: 'yes', name: 'Yes' },
|
||||||
|
{ id: 'no', name: 'No' }
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return empty string for null', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, null, options);
|
||||||
|
expect(service.format(field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for empty string', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, '', options);
|
||||||
|
expect(service.formatValue('', field)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape A: should look up label for string id', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, 'yes', options);
|
||||||
|
expect(service.format(field)).toBe('Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape A fallback: should return the id when not found', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, 'unknown', options);
|
||||||
|
expect(service.format(field)).toBe('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape B: should use .name from FormFieldOption object (post-click value)', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, { id: 'yes', name: 'Yes' }, options);
|
||||||
|
expect(service.format(field)).toBe('Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shape B no name: should JSON.stringify', () => {
|
||||||
|
const field = makeField(FormFieldTypes.RADIO_BUTTONS, { id: 'yes' }, options);
|
||||||
|
expect(service.format(field)).toBe('{"id":"yes"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('register', () => {
|
||||||
|
it('should allow overriding a registered formatter', () => {
|
||||||
|
service.register(FormFieldTypes.PEOPLE, () => 'custom-override');
|
||||||
|
const field = makeField(FormFieldTypes.PEOPLE, [{ firstName: 'Alice' }]);
|
||||||
|
expect(service.format(field)).toBe('custom-override');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2025 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 { Injectable } from '@angular/core';
|
||||||
|
import { FormFieldModel } from '../components/widgets/core/form-field.model';
|
||||||
|
import { FormFieldTypes } from '../components/widgets/core/form-field-types';
|
||||||
|
import { FormFieldOption } from '../components/widgets/core/form-field-option';
|
||||||
|
import { GroupModel } from '../components/widgets/core/group.model';
|
||||||
|
import { FullNamePipe } from '../../pipes/full-name.pipe';
|
||||||
|
import { UserLike } from '../../pipes/user-like.interface';
|
||||||
|
|
||||||
|
export type FormFieldValueFormatter = (value: any, field: FormFieldModel) => string;
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class FormFieldValueFormatterService {
|
||||||
|
private readonly formatters = new Map<string, FormFieldValueFormatter>();
|
||||||
|
private readonly fullNamePipe = new FullNamePipe();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.register(FormFieldTypes.PEOPLE, (value) => this.formatPeople(value));
|
||||||
|
this.register(FormFieldTypes.FUNCTIONAL_GROUP, (value) => this.formatGroup(value));
|
||||||
|
this.register(FormFieldTypes.DROPDOWN, (value, field) => this.formatDropdown(value, field));
|
||||||
|
this.register(FormFieldTypes.RADIO_BUTTONS, (value, field) => this.formatRadio(value, field));
|
||||||
|
}
|
||||||
|
|
||||||
|
register(fieldType: string, formatter: FormFieldValueFormatter): void {
|
||||||
|
this.formatters.set(fieldType, formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFormatter(fieldType: string): boolean {
|
||||||
|
return this.formatters.has(fieldType);
|
||||||
|
}
|
||||||
|
|
||||||
|
format(field: FormFieldModel): string {
|
||||||
|
return this.formatValue(field.value, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValue(value: any, field: FormFieldModel): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const formatter = this.formatters.get(field.type);
|
||||||
|
if (formatter) {
|
||||||
|
return formatter(value, field);
|
||||||
|
}
|
||||||
|
return this.stringifyValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
stringifyValue(value: any): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatPeople(value: UserLike | UserLike[]): string {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const users = Array.isArray(value) ? value : [value];
|
||||||
|
if (users.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
.map((u) => this.fullNamePipe.transform(u))
|
||||||
|
.filter((s) => !!s)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatGroup(value: GroupModel | GroupModel[]): string {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const groups = Array.isArray(value) ? value : [value];
|
||||||
|
if (groups.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
.map((g) => g?.name ?? '')
|
||||||
|
.filter((s) => !!s)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDropdown(value: string | FormFieldOption | Array<string | FormFieldOption>, field: FormFieldModel): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const option = field.options?.find((o) => o.id === value);
|
||||||
|
return option?.name ?? value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
.map((v) => {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return field.options?.find((o) => o.id === v)?.name ?? v;
|
||||||
|
}
|
||||||
|
return v?.name ?? '';
|
||||||
|
})
|
||||||
|
.filter((s) => !!s)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
return value?.name ?? JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatRadio(value: string | FormFieldOption, field: FormFieldModel): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const option = field.options?.find((o) => o.id === value);
|
||||||
|
return option?.name ?? value;
|
||||||
|
}
|
||||||
|
return value?.name ?? JSON.stringify(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2025 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 { InjectionToken } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export const ADF_TYPED_VALUE_FORMATTING_ENABLED = new InjectionToken<Observable<boolean> | boolean>('adf-typed-value-formatting-enabled');
|
||||||
+82
-1
@@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FormFieldModel, FormModel, FormFieldTypes, UnitTestingUtils } from '@alfresco/adf-core';
|
import { FormFieldModel, FormModel, FormFieldTypes, UnitTestingUtils, ADF_TYPED_VALUE_FORMATTING_ENABLED } from '@alfresco/adf-core';
|
||||||
import { HarnessLoader } from '@angular/cdk/testing';
|
import { HarnessLoader } from '@angular/cdk/testing';
|
||||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
@@ -207,4 +207,85 @@ describe('DisplayExternalPropertyWidgetComponent', () => {
|
|||||||
expect(adfLeftLabel).toBeNull();
|
expect(adfLeftLabel).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('typed value formatting', () => {
|
||||||
|
describe('when flag is on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DisplayExternalPropertyWidgetComponent],
|
||||||
|
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DisplayExternalPropertyWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display formatted full name for a People value', async () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
readOnly: true,
|
||||||
|
value: [{ firstName: 'Alyssa', lastName: 'Adcock' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = await loader.getHarness(MatInputHarness);
|
||||||
|
expect(await input.getValue()).toBe('Alyssa Adcock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display comma-separated group names for a Group value', async () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||||
|
type: FormFieldTypes.FUNCTIONAL_GROUP,
|
||||||
|
readOnly: true,
|
||||||
|
value: [{ name: 'Eng' }, { name: 'QA' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = await loader.getHarness(MatInputHarness);
|
||||||
|
expect(await input.getValue()).toBe('Eng, QA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not contain [object Object] for a complex value', async () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||||
|
type: FormFieldTypes.PEOPLE,
|
||||||
|
readOnly: true,
|
||||||
|
value: [{ firstName: 'Alice', lastName: 'Brown' }]
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = await loader.getHarness(MatInputHarness);
|
||||||
|
expect(await input.getValue()).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not JSON-stringify a Date value for an unregistered type', async () => {
|
||||||
|
const date = new Date('2026-06-02T14:30:00.000Z');
|
||||||
|
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||||
|
type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY,
|
||||||
|
readOnly: true,
|
||||||
|
externalProperty: 'prop',
|
||||||
|
value: date
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = await loader.getHarness(MatInputHarness);
|
||||||
|
expect(await input.getValue()).toBe(String(date));
|
||||||
|
expect(await input.getValue()).not.toContain('"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag is off', () => {
|
||||||
|
it('should leave raw string value unchanged (default behaviour preserved)', async () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||||
|
type: FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY,
|
||||||
|
readOnly: true,
|
||||||
|
externalProperty: 'prop',
|
||||||
|
value: 'banana'
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = await loader.getHarness(MatInputHarness);
|
||||||
|
expect(await input.getValue()).toBe('banana');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-4
@@ -15,14 +15,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { WidgetComponent, FormBaseModule } from '@alfresco/adf-core';
|
import { WidgetComponent, FormBaseModule, FormFieldValueFormatterService, ADF_TYPED_VALUE_FORMATTING_ENABLED } from '@alfresco/adf-core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { isObservable } from 'rxjs';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -50,18 +52,41 @@ export class DisplayExternalPropertyWidgetComponent extends WidgetComponent impl
|
|||||||
propertyControl: FormControl;
|
propertyControl: FormControl;
|
||||||
|
|
||||||
private readonly formCloudService = inject(FormCloudService);
|
private readonly formCloudService = inject(FormCloudService);
|
||||||
|
private readonly formatter = inject(FormFieldValueFormatterService);
|
||||||
|
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private formattingEnabled = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (isObservable(this.formattingEnabledToken)) {
|
||||||
|
this.formattingEnabledToken.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||||
|
this.formattingEnabled = enabled ?? false;
|
||||||
|
if (this.propertyControl) {
|
||||||
|
this.propertyControl.setValue(this.computeDisplayValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formattingEnabled = this.formattingEnabledToken ?? false;
|
||||||
|
}
|
||||||
this.initFormControl();
|
this.initFormControl();
|
||||||
this.initPreviewState();
|
this.initPreviewState();
|
||||||
this.handleFailedPropertyLoad();
|
this.handleFailedPropertyLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeDisplayValue(): unknown {
|
||||||
|
const value = this.field?.value;
|
||||||
|
const isFormattableValue = value != null && typeof value !== 'string';
|
||||||
|
if (this.formattingEnabled && isFormattableValue && this.formatter.hasFormatter(this.field?.type ?? '')) {
|
||||||
|
return this.formatter.format(this.field);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private initFormControl(): void {
|
private initFormControl(): void {
|
||||||
this.propertyControl = new FormControl(
|
this.propertyControl = new FormControl(
|
||||||
{
|
{
|
||||||
value: this.field?.value,
|
value: this.computeDisplayValue(),
|
||||||
disabled: this.field?.readOnly || this.readOnly
|
disabled: !!(this.field?.readOnly || this.readOnly)
|
||||||
},
|
},
|
||||||
this.isRequired() ? [Validators.required] : []
|
this.isRequired() ? [Validators.required] : []
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@
|
|||||||
[id]="'readonlyOption-' + field.id"
|
[id]="'readonlyOption-' + field.id"
|
||||||
[value]="field.value"
|
[value]="field.value"
|
||||||
>
|
>
|
||||||
{{field.value}}
|
{{readOnlyDisplayValue}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
|
|||||||
+98
-1
@@ -27,7 +27,8 @@ import {
|
|||||||
FormFieldTypes,
|
FormFieldTypes,
|
||||||
UnitTestingUtils,
|
UnitTestingUtils,
|
||||||
FormFieldComponent,
|
FormFieldComponent,
|
||||||
FormRenderingService
|
FormRenderingService,
|
||||||
|
ADF_TYPED_VALUE_FORMATTING_ENABLED
|
||||||
} from '@alfresco/adf-core';
|
} from '@alfresco/adf-core';
|
||||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||||
import {
|
import {
|
||||||
@@ -1515,4 +1516,100 @@ describe('DropdownCloudWidgetComponent instantiated by FormFieldComponent wrappe
|
|||||||
expect(selectedOption).toEqual('option1');
|
expect(selectedOption).toEqual('option1');
|
||||||
expect(setValueSpy).toHaveBeenCalledTimes(1);
|
expect(setValueSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('typed value formatting (readOnlyDisplayValue)', () => {
|
||||||
|
const options = [
|
||||||
|
{ id: 'a', name: 'Apple' },
|
||||||
|
{ id: 'b', name: 'Banana' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let fixture: ComponentFixture<DropdownCloudWidgetComponent>;
|
||||||
|
let widget: DropdownCloudWidgetComponent;
|
||||||
|
|
||||||
|
describe('when flag is on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DropdownCloudWidgetComponent],
|
||||||
|
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DropdownCloudWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return formatted label for an object value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: { id: 'a', name: 'Apple' }
|
||||||
|
});
|
||||||
|
widget.field.options = options;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.readOnlyDisplayValue).toBe('Apple');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return comma-separated labels for an array value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: [
|
||||||
|
{ id: 'a', name: 'Apple' },
|
||||||
|
{ id: 'b', name: 'Banana' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
widget.field.options = options;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.readOnlyDisplayValue).toBe('Apple, Banana');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not contain [object Object] for an object value', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: { id: 'a', name: 'Apple' }
|
||||||
|
});
|
||||||
|
widget.field.options = options;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.readOnlyDisplayValue).not.toContain('[object Object]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a plain string value to its matching option label', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: 'a'
|
||||||
|
});
|
||||||
|
widget.field.options = options;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.readOnlyDisplayValue).toBe('Apple');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when flag is off', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DropdownCloudWidgetComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DropdownCloudWidgetComponent);
|
||||||
|
widget = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the raw field value (default behaviour preserved)', () => {
|
||||||
|
widget.field = new FormFieldModel(new FormModel(), {
|
||||||
|
id: 'dropdown-field',
|
||||||
|
type: FormFieldTypes.DROPDOWN,
|
||||||
|
value: { id: 'a', name: 'Apple' }
|
||||||
|
});
|
||||||
|
widget.field.options = options;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.readOnlyDisplayValue).not.toBe('Apple');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+27
-2
@@ -23,6 +23,8 @@ import {
|
|||||||
FormFieldModel,
|
FormFieldModel,
|
||||||
FormFieldOption,
|
FormFieldOption,
|
||||||
FormFieldTypes,
|
FormFieldTypes,
|
||||||
|
FormFieldValueFormatterService,
|
||||||
|
ADF_TYPED_VALUE_FORMATTING_ENABLED,
|
||||||
FormService,
|
FormService,
|
||||||
ReactiveFormWidget,
|
ReactiveFormWidget,
|
||||||
RuleEntry,
|
RuleEntry,
|
||||||
@@ -31,15 +33,15 @@ import {
|
|||||||
} from '@alfresco/adf-core';
|
} from '@alfresco/adf-core';
|
||||||
import { AsyncPipe, NgClass } from '@angular/common';
|
import { AsyncPipe, NgClass } from '@angular/common';
|
||||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Subject } from 'rxjs';
|
import { BehaviorSubject, isObservable, Subject } from 'rxjs';
|
||||||
import { debounceTime, filter, map } from 'rxjs/operators';
|
import { debounceTime, filter, map } from 'rxjs/operators';
|
||||||
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { FormUtilsService } from '../../../services/form-utils.service';
|
import { FormUtilsService } from '../../../services/form-utils.service';
|
||||||
import { defaultValueValidator } from './validators';
|
import { defaultValueValidator } from './validators';
|
||||||
|
|
||||||
@@ -77,6 +79,10 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
|||||||
private readonly appConfig = inject(AppConfigService);
|
private readonly appConfig = inject(AppConfigService);
|
||||||
private readonly formUtilsService = inject(FormUtilsService);
|
private readonly formUtilsService = inject(FormUtilsService);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly formatter = inject(FormFieldValueFormatterService);
|
||||||
|
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
|
||||||
|
private formattingEnabled = false;
|
||||||
|
readOnlyDisplayValue: string | undefined;
|
||||||
|
|
||||||
typeId = 'DropdownCloudWidgetComponent';
|
typeId = 'DropdownCloudWidgetComponent';
|
||||||
showInputFilter = false;
|
showInputFilter = false;
|
||||||
@@ -132,6 +138,16 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (isObservable(this.formattingEnabledToken)) {
|
||||||
|
this.formattingEnabledToken.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||||
|
this.formattingEnabled = enabled ?? false;
|
||||||
|
this.readOnlyDisplayValue = this.computeReadOnlyDisplayValue();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formattingEnabled = this.formattingEnabledToken ?? false;
|
||||||
|
this.readOnlyDisplayValue = this.computeReadOnlyDisplayValue();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
We can have a lot of 'control.setValue' caused by form rules events
|
We can have a lot of 'control.setValue' caused by form rules events
|
||||||
e.g. every time if we focusin/focusout etc. we are calling a setValue.
|
e.g. every time if we focusin/focusout etc. we are calling a setValue.
|
||||||
@@ -161,8 +177,17 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeReadOnlyDisplayValue(): string | undefined {
|
||||||
|
const value = this.field.value;
|
||||||
|
const isFormattableValue = value != null && (typeof value !== 'string' || this.formatter.hasFormatter(this.field.type));
|
||||||
|
const shouldFormatValue = this.formattingEnabled && isFormattableValue;
|
||||||
|
|
||||||
|
return shouldFormatValue ? this.formatter.format(this.field) : value;
|
||||||
|
}
|
||||||
|
|
||||||
updateReactiveFormControl(): void {
|
updateReactiveFormControl(): void {
|
||||||
this.setFormControlValue();
|
this.setFormControlValue();
|
||||||
|
this.readOnlyDisplayValue = this.computeReadOnlyDisplayValue();
|
||||||
|
|
||||||
this.updateFormControlState();
|
this.updateFormControlState();
|
||||||
if (this.field?.form?.showAllValidationErrors) {
|
if (this.field?.form?.showAllValidationErrors) {
|
||||||
|
|||||||
Reference in New Issue
Block a user