mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-26 17:24:56 +00:00
AAE-23521 Fix improve dropdown reactive form (#10040)
* AAE-23521 Fix improve dropdown reactive form * AAE-23521 Update process services dropdown * AAE-23521 pr suggestions * AAE-23521 move tests * AAE-23521 fixes
This commit is contained in:
parent
5930a27bbd
commit
f88ff10f30
@ -54,7 +54,7 @@ export class FormFieldTypes {
|
||||
|
||||
static VALIDATABLE_TYPES: string[] = [FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY];
|
||||
|
||||
static REACTIVE_TYPES: string[] = [FormFieldTypes.DATE, FormFieldTypes.DATETIME];
|
||||
static REACTIVE_TYPES: string[] = [FormFieldTypes.DATE, FormFieldTypes.DATETIME, FormFieldTypes.DROPDOWN];
|
||||
|
||||
static CONSTANT_VALUE_TYPES: string[] = [FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY];
|
||||
|
||||
|
@ -59,51 +59,6 @@ describe('FormFieldValidator', () => {
|
||||
expect(validator.validate(field)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail (display error) for multiple type dropdown with zero selection', () => {
|
||||
const field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: [{ id: 'id_cat', name: 'Cat' }],
|
||||
required: true,
|
||||
selectionType: 'multiple',
|
||||
hasEmptyValue: false,
|
||||
options: [
|
||||
{ id: 'id_cat', name: 'Cat' },
|
||||
{ id: 'id_dog', name: 'Dog' }
|
||||
]
|
||||
});
|
||||
|
||||
const validateBeforeUnselect = validator.validate(field);
|
||||
field.value = [];
|
||||
const validateAfterUnselect = validator.validate(field);
|
||||
|
||||
expect(validateBeforeUnselect).toBe(true);
|
||||
expect(validateAfterUnselect).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail (display error) for dropdown with null value', () => {
|
||||
const field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: null,
|
||||
required: true,
|
||||
options: [{ id: 'one', name: 'one' }],
|
||||
selectionType: 'multiple'
|
||||
});
|
||||
|
||||
expect(validator.validate(field)).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail (display error) for dropdown with empty object', () => {
|
||||
const field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: {},
|
||||
required: true,
|
||||
options: [{ id: 'one', name: 'one' }],
|
||||
selectionType: 'multiple'
|
||||
});
|
||||
|
||||
expect(validator.validate(field)).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail (display error) for radio buttons', () => {
|
||||
const field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.RADIO_BUTTONS,
|
||||
|
@ -33,7 +33,6 @@ export class RequiredFieldValidator implements FormFieldValidator {
|
||||
FormFieldTypes.NUMBER,
|
||||
FormFieldTypes.BOOLEAN,
|
||||
FormFieldTypes.TYPEAHEAD,
|
||||
FormFieldTypes.DROPDOWN,
|
||||
FormFieldTypes.PEOPLE,
|
||||
FormFieldTypes.FUNCTIONAL_GROUP,
|
||||
FormFieldTypes.RADIO_BUTTONS,
|
||||
@ -51,22 +50,6 @@ export class RequiredFieldValidator implements FormFieldValidator {
|
||||
|
||||
validate(field: FormFieldModel): boolean {
|
||||
if (this.isSupported(field) && field.isVisible) {
|
||||
if (field.type === FormFieldTypes.DROPDOWN) {
|
||||
if (field.hasMultipleValues) {
|
||||
return Array.isArray(field.value) && !!field.value.length;
|
||||
}
|
||||
|
||||
if (field.hasEmptyValue && field.emptyOption) {
|
||||
if (field.value === field.emptyOption.id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.required && field.value && typeof field.value === 'object' && !Object.keys(field.value).length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FormFieldTypes.RADIO_BUTTONS) {
|
||||
const option = field.options.find((opt) => opt.id === field.value);
|
||||
return !!option;
|
||||
|
@ -1,5 +1,6 @@
|
||||
.adf-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-widget-container {
|
||||
height: auto;
|
||||
|
@ -1,43 +1,50 @@
|
||||
<div class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="(!field.isValid && isTouched()) || isRestApiFailed" [class.adf-readonly]="field.readOnly" [class.adf-left-label-input-container]="field.leftLabels">
|
||||
<div class="adf-dropdown-widget-top-labels">
|
||||
<label class="adf-label" [attr.for]="field.id" [class.adf-left-label]="field.leftLabels">{{field.name | translate }}<span class="adf-asterisk"
|
||||
*ngIf="isRequired()">*</span>
|
||||
<div
|
||||
class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="dropdownControl.invalid && dropdownControl.touched"
|
||||
[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>
|
||||
<mat-label *ngIf="getDefaultOption(list$ | async) as defaultOption">
|
||||
{{ defaultOption.name }}
|
||||
</mat-label>
|
||||
<mat-select class="adf-select"
|
||||
[id]="field.id"
|
||||
[(ngModel)]="field.value"
|
||||
[disabled]="field.readOnly"
|
||||
[compareWith]="compareDropdownValues"
|
||||
(ngModelChange)="selectionChangedForField(field)"
|
||||
[title]="field.tooltip"
|
||||
[required]="isRequired()"
|
||||
panelClass="adf-select-filter"
|
||||
(blur)="markAsTouched()"
|
||||
[multiple]="field.hasMultipleValues">
|
||||
<label *ngIf="!field.leftLabels" class="adf-label" [attr.for]="field.id">
|
||||
{{ field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span>
|
||||
</label>
|
||||
<mat-select
|
||||
class="adf-select"
|
||||
[formControl]="dropdownControl"
|
||||
[id]="field.id"
|
||||
[compareWith]="compareDropdownValues"
|
||||
[title]="field.tooltip"
|
||||
panelClass="adf-select-filter"
|
||||
[multiple]="field.hasMultipleValues"
|
||||
>
|
||||
<adf-select-filter-input *ngIf="showInputFilter" (change)="filter$.next($event)"></adf-select-filter-input>
|
||||
|
||||
<mat-option *ngFor="let opt of list$ | async"
|
||||
[value]="getOptionValue(opt, field.value)"
|
||||
[id]="opt.id">{{opt.name}}
|
||||
</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</mat-option>
|
||||
<mat-option *ngFor="let opt of list$ | async" [value]="opt" [id]="opt.id">{{opt.name}}</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="isReadOnlyType" [value]="field.value">{{field.value}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div *ngIf="!previewState && !isReadOnlyField">
|
||||
<error-widget [error]="field.validationSummary"></error-widget>
|
||||
<error-widget class="adf-dropdown-required-message" *ngIf="showRequiredMessage()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
<error-widget class="adf-dropdown-failed-message" *ngIf="isRestApiFailed"
|
||||
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"></error-widget>
|
||||
<error-widget class="adf-dropdown-failed-message" *ngIf="variableOptionsFailed"
|
||||
required="{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}"></error-widget>
|
||||
<div *ngIf="!previewState && !field.readOnly">
|
||||
<error-widget
|
||||
class="adf-dropdown-required-message"
|
||||
*ngIf="showRequiredMessage"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
></error-widget>
|
||||
<error-widget
|
||||
class="adf-dropdown-failed-message"
|
||||
*ngIf="isRestApiFailed"
|
||||
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"
|
||||
></error-widget>
|
||||
<error-widget
|
||||
class="adf-dropdown-failed-message"
|
||||
*ngIf="variableOptionsFailed"
|
||||
required="{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}"
|
||||
></error-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import 'styles/mat-selectors';
|
||||
|
||||
.adf {
|
||||
&-dropdown-widget {
|
||||
width: 100%;
|
||||
@ -25,5 +27,9 @@
|
||||
&-dropdown-failed-message .adf-error-container {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
#{$mat-select-arrow-wrapper} {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatSelectHarness } from '@angular/material/select/testing';
|
||||
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
describe('DropdownCloudWidgetComponent', () => {
|
||||
@ -95,7 +94,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
it('should select the default value when an option is chosen as default', async () => {
|
||||
widget.field.value = 'option_2';
|
||||
|
||||
expect(widget.fieldValue).toEqual('option_2');
|
||||
expect(widget.field.value).toEqual('option_2');
|
||||
});
|
||||
|
||||
it('should select the empty value when no default is chosen', async () => {
|
||||
@ -103,7 +102,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.open();
|
||||
|
||||
expect(widget.fieldValue).toEqual('empty');
|
||||
expect(widget.field.value).toEqual('empty');
|
||||
});
|
||||
|
||||
it('should load data from restUrl and populate options', async () => {
|
||||
@ -295,17 +294,14 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
await dropdown.clickOptions({ selector: '[id="opt_1"]' });
|
||||
|
||||
expect(await dropdown.getValueText()).toEqual('option_1');
|
||||
expect(widget.fieldValue).toEqual('opt_1');
|
||||
expect(widget.field.value).toEqual('opt_1');
|
||||
|
||||
await dropdown.open();
|
||||
await dropdown.clickOptions({ selector: '[id="empty"]' });
|
||||
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const dropdownLabel = await formField.getLabel();
|
||||
expect(widget.field.value).toEqual(undefined);
|
||||
|
||||
expect(dropdownLabel).toEqual('This is a mock none option');
|
||||
expect(widget.fieldValue).toEqual(undefined);
|
||||
expect(await dropdown.getValueText()).toEqual('');
|
||||
expect(await dropdown.getValueText()).toEqual('This is a mock none option');
|
||||
});
|
||||
});
|
||||
|
||||
@ -528,6 +524,58 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
{ id: 'opt_4', name: 'option_4' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail (display error) for multiple type dropdown with zero selection', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', readOnly: 'false' }), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: [{ id: 'id_cat', name: 'Cat' }],
|
||||
required: true,
|
||||
selectionType: 'multiple',
|
||||
hasEmptyValue: false,
|
||||
options: [
|
||||
{ id: 'id_cat', name: 'Cat' },
|
||||
{ id: 'id_dog', name: 'Dog' }
|
||||
]
|
||||
});
|
||||
|
||||
const validateBeforeUnselect = widget.dropdownControl.valid;
|
||||
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.clickOptions({ selector: '[id="id_cat"]' });
|
||||
|
||||
const validateAfterUnselect = widget.dropdownControl.valid;
|
||||
|
||||
expect(validateBeforeUnselect).toBe(true);
|
||||
expect(validateAfterUnselect).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail (display error) for dropdown with null value', () => {
|
||||
widget.field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: null,
|
||||
required: true,
|
||||
options: [{ id: 'one', name: 'one' }],
|
||||
selectionType: 'multiple'
|
||||
});
|
||||
|
||||
widget.ngOnInit();
|
||||
|
||||
expect(widget.dropdownControl.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail (display error) for dropdown with empty object', () => {
|
||||
widget.field = new FormFieldModel(new FormModel(), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
value: {},
|
||||
required: true,
|
||||
options: [{ id: 'one', name: 'one' }],
|
||||
selectionType: 'multiple'
|
||||
});
|
||||
|
||||
widget.ngOnInit();
|
||||
|
||||
expect(widget.dropdownControl.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Linked Dropdown', () => {
|
||||
@ -697,7 +745,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.options).toEqual(mockConditionalEntries[0].options);
|
||||
expect(widget.fieldValue).toEqual('');
|
||||
expect(widget.field.value).toEqual('');
|
||||
});
|
||||
|
||||
it('should not reset the current value when it is part of the available options', () => {
|
||||
@ -710,7 +758,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.options).toEqual(mockConditionalEntries[0].options);
|
||||
expect(widget.fieldValue).toEqual('ATH');
|
||||
expect(widget.field.value).toEqual('ATH');
|
||||
});
|
||||
|
||||
it('should fire a form field value changed event when the value gets reset (notify children on the chain to reset)', () => {
|
||||
|
@ -15,9 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
AppConfigService,
|
||||
CardViewModule,
|
||||
ErrorMessageModel,
|
||||
ErrorWidgetComponent,
|
||||
FormFieldEvent,
|
||||
FormFieldModel,
|
||||
FormFieldOption,
|
||||
@ -26,10 +28,16 @@ import {
|
||||
RuleEntry,
|
||||
WidgetComponent
|
||||
} from '@alfresco/adf-core';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
|
||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { filter, map, takeUntil } from 'rxjs/operators';
|
||||
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
|
||||
export const DEFAULT_OPTION = {
|
||||
id: 'empty',
|
||||
@ -43,27 +51,34 @@ export const HIDE_FILTER_LIMIT = 5;
|
||||
selector: 'dropdown-cloud-widget',
|
||||
templateUrl: './dropdown-cloud.widget.html',
|
||||
styleUrls: ['./dropdown-cloud.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
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
NgFor,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
ErrorWidgetComponent,
|
||||
TranslateModule,
|
||||
CardViewModule // imported for adf-select-filter-input
|
||||
]
|
||||
})
|
||||
export class DropdownCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
|
||||
public formService = inject(FormService);
|
||||
private formCloudService = inject(FormCloudService);
|
||||
private appConfig = inject(AppConfigService);
|
||||
|
||||
typeId = 'DropdownCloudWidgetComponent';
|
||||
showInputFilter = false;
|
||||
isRestApiFailed = false;
|
||||
variableOptionsFailed = false;
|
||||
previewState = false;
|
||||
restApiHostName: string;
|
||||
list$: Observable<FormFieldOption[]>;
|
||||
dropdownControl = new FormControl<FormFieldOption | FormFieldOption[]>(undefined);
|
||||
|
||||
list$ = new BehaviorSubject<FormFieldOption[]>([]);
|
||||
filter$ = new BehaviorSubject<string>('');
|
||||
|
||||
private readonly defaultVariableOptionId = 'id';
|
||||
@ -72,30 +87,149 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
|
||||
protected onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(public formService: FormService, private formCloudService: FormCloudService, private appConfig: AppConfigService) {
|
||||
super(formService);
|
||||
get showRequiredMessage(): boolean {
|
||||
return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
|
||||
}
|
||||
|
||||
get isReadOnlyType(): boolean {
|
||||
return this.field.type === 'readonly';
|
||||
}
|
||||
|
||||
private get isLinkedWidget(): boolean {
|
||||
return !!this.linkedWidgetId;
|
||||
}
|
||||
|
||||
private get linkedWidgetId(): string {
|
||||
return this.field?.rule?.ruleOn;
|
||||
}
|
||||
|
||||
private get isReadOnlyForm(): boolean {
|
||||
return !!this.field?.form?.readOnly;
|
||||
}
|
||||
|
||||
private get hasRestUrl(): boolean {
|
||||
return !!this.field?.restUrl;
|
||||
}
|
||||
|
||||
private get isValidRestConfig(): boolean {
|
||||
return this.isRestOptionType && this.hasRestUrl;
|
||||
}
|
||||
|
||||
private get isRestOptionType(): boolean {
|
||||
return this.field?.optionType === 'rest';
|
||||
}
|
||||
|
||||
private get isVariableOptionType(): boolean {
|
||||
return this.field?.optionType === 'variable';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setPreviewState();
|
||||
|
||||
this.checkFieldOptionsSource();
|
||||
this.updateOptions();
|
||||
|
||||
this.initFormControl();
|
||||
this.initFilter();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
compareDropdownValues(opt1: FormFieldOption | string, opt2: FormFieldOption | string): boolean {
|
||||
if (!opt1 || !opt2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'string' && typeof opt2 === 'object') {
|
||||
return opt1 === opt2.id || opt1 === opt2.name;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'object' && typeof opt2 === 'string') {
|
||||
return opt1.id === opt2 || opt1.name === opt2;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'object' && typeof opt2 === 'object') {
|
||||
return opt1.id === opt2.id || opt1.name === opt2.name;
|
||||
}
|
||||
|
||||
return opt1 === opt2;
|
||||
}
|
||||
|
||||
selectionChangedForField(field: FormFieldModel): void {
|
||||
const formFieldValueChangedEvent = new FormFieldEvent(field.form, field);
|
||||
this.formService.formFieldValueChanged.next(formFieldValueChangedEvent);
|
||||
this.onFieldChanged(field);
|
||||
}
|
||||
|
||||
private initFormControl(): void {
|
||||
if (this.field?.required) {
|
||||
this.dropdownControl.addValidators([Validators.required]);
|
||||
}
|
||||
|
||||
if (this.field?.readOnly || this.readOnly) {
|
||||
this.dropdownControl.disable({ emitEvent: false });
|
||||
}
|
||||
|
||||
this.dropdownControl.valueChanges
|
||||
.pipe(
|
||||
filter(() => !!this.field),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this.setOptionValue(value, this.field);
|
||||
this.selectionChangedForField(this.field);
|
||||
});
|
||||
|
||||
this.dropdownControl.statusChanges
|
||||
.pipe(
|
||||
filter(() => !!this.field),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe(() => this.handleErrors());
|
||||
|
||||
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
|
||||
this.handleErrors();
|
||||
}
|
||||
|
||||
private handleErrors(): void {
|
||||
if (this.dropdownControl.valid) {
|
||||
this.field.validationSummary = new ErrorMessageModel('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dropdownControl.invalid && this.dropdownControl.errors.required) {
|
||||
this.field.validationSummary = new ErrorMessageModel({ message: 'FORM.FIELD.REQUIRED' });
|
||||
}
|
||||
}
|
||||
|
||||
private initFilter(): void {
|
||||
this.filter$
|
||||
.pipe(
|
||||
filter((search) => search !== undefined),
|
||||
map((search) =>
|
||||
search ? this.field.options.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase())) : this.field.options
|
||||
),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe((result) => this.list$.next(result));
|
||||
}
|
||||
|
||||
private checkFieldOptionsSource(): void {
|
||||
switch (true) {
|
||||
case this.isReadOnlyForm():
|
||||
case this.isReadOnlyForm:
|
||||
break;
|
||||
|
||||
case this.isValidRestConfig() && !this.isLinkedWidget():
|
||||
case this.isValidRestConfig && !this.isLinkedWidget:
|
||||
this.persistFieldOptionsFromRestApi();
|
||||
break;
|
||||
|
||||
case this.isLinkedWidget():
|
||||
case this.isLinkedWidget:
|
||||
this.loadFieldOptionsForLinkedWidget();
|
||||
break;
|
||||
|
||||
case this.isVariableOptionType():
|
||||
case this.isVariableOptionType:
|
||||
this.persistFieldOptionsFromVariable();
|
||||
break;
|
||||
|
||||
@ -114,7 +248,7 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
|
||||
if (dropdownOptions) {
|
||||
const formVariableOptions: FormFieldOption[] = this.getOptionsFromPath(dropdownOptions, optionsPath);
|
||||
this.field.options = formVariableOptions;
|
||||
this.updateOptions(formVariableOptions);
|
||||
this.resetInvalidValue();
|
||||
this.field.updateForm();
|
||||
} else {
|
||||
@ -189,42 +323,33 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
?.value;
|
||||
}
|
||||
|
||||
private isVariableOptionType(): boolean {
|
||||
return this.field?.optionType === 'variable';
|
||||
}
|
||||
|
||||
private isRestOptionType(): boolean {
|
||||
return this.field?.optionType === 'rest';
|
||||
}
|
||||
|
||||
private persistFieldOptionsFromRestApi() {
|
||||
if (this.isValidRestConfig()) {
|
||||
if (this.isValidRestConfig) {
|
||||
this.resetRestApiErrorMessage();
|
||||
const bodyParam = this.buildBodyParam();
|
||||
this.formCloudService
|
||||
.getRestWidgetData(this.field.form.id, this.field.id, bodyParam)
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(
|
||||
(result: FormFieldOption[]) => {
|
||||
.subscribe({
|
||||
next: (result: FormFieldOption[]) => {
|
||||
this.resetRestApiErrorMessage();
|
||||
this.field.options = result;
|
||||
this.updateOptions();
|
||||
this.updateOptions(result);
|
||||
this.field.updateForm();
|
||||
this.resetInvalidValue();
|
||||
},
|
||||
(err) => {
|
||||
error: (err) => {
|
||||
this.resetRestApiOptions();
|
||||
this.handleError(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private buildBodyParam(): any {
|
||||
const bodyParam = Object.assign({});
|
||||
if (this.isLinkedWidget()) {
|
||||
if (this.isLinkedWidget) {
|
||||
const parentWidgetValue = this.getParentWidgetValue();
|
||||
const parentWidgetId = this.getLinkedWidgetId();
|
||||
const parentWidgetId = this.linkedWidgetId;
|
||||
bodyParam[parentWidgetId] = parentWidgetValue;
|
||||
}
|
||||
return bodyParam;
|
||||
@ -246,20 +371,20 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
|
||||
private getParentWidgetValue(): string {
|
||||
const parentWidgetId = this.getLinkedWidgetId();
|
||||
const parentWidgetId = this.linkedWidgetId;
|
||||
const parentWidget = this.getFormFieldById(parentWidgetId);
|
||||
return parentWidget?.value;
|
||||
}
|
||||
|
||||
private parentValueChanged(value: string) {
|
||||
if (value && !this.isNoneValueSelected(value)) {
|
||||
this.isValidRestConfig() ? this.persistFieldOptionsFromRestApi() : this.persistFieldOptionsFromManualList(value);
|
||||
this.isValidRestConfig ? this.persistFieldOptionsFromRestApi() : this.persistFieldOptionsFromManualList(value);
|
||||
} else if (this.isNoneValueSelected(value)) {
|
||||
this.resetRestApiErrorMessage();
|
||||
this.resetOptions();
|
||||
this.resetInvalidValue();
|
||||
} else {
|
||||
this.field.options = [];
|
||||
this.updateOptions([]);
|
||||
this.resetInvalidValue();
|
||||
}
|
||||
}
|
||||
@ -277,7 +402,7 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
const rulesEntries = this.field.rule.entries;
|
||||
rulesEntries.forEach((ruleEntry: RuleEntry) => {
|
||||
if (ruleEntry.key === value) {
|
||||
this.field.options = ruleEntry.options;
|
||||
this.updateOptions(ruleEntry.options);
|
||||
this.resetInvalidValue();
|
||||
this.field.updateForm();
|
||||
}
|
||||
@ -299,92 +424,51 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
|
||||
private isValidValue(): boolean {
|
||||
return this.fieldValue && this.isSelectedValueInOptions();
|
||||
return this.field.value && this.isSelectedValueInOptions();
|
||||
}
|
||||
|
||||
private isSelectedValueInOptions(): boolean {
|
||||
if (Array.isArray(this.fieldValue)) {
|
||||
if (Array.isArray(this.field.value)) {
|
||||
const optionIdList = [...this.field.options].map((option) => option.id);
|
||||
const fieldValueIds = this.fieldValue.map((valueOption) => valueOption.id);
|
||||
const fieldValueIds = this.field.value.map((valueOption) => valueOption.id);
|
||||
return fieldValueIds.every((valueOptionId) => optionIdList.includes(valueOptionId));
|
||||
} else {
|
||||
return [...this.field.options].map((option) => option.id).includes(this.fieldValue);
|
||||
return [...this.field.options].map((option) => option.id).includes(this.field.value);
|
||||
}
|
||||
}
|
||||
|
||||
get fieldValue(): string {
|
||||
return this.field.value;
|
||||
}
|
||||
|
||||
private hasRuleEntries(): boolean {
|
||||
return !!this.field.rule.entries.length;
|
||||
}
|
||||
|
||||
private resetOptions() {
|
||||
this.field.options = [];
|
||||
this.updateOptions();
|
||||
}
|
||||
|
||||
selectionChangedForField(field: FormFieldModel) {
|
||||
const formFieldValueChangedEvent = new FormFieldEvent(field.form, field);
|
||||
this.formService.formFieldValueChanged.next(formFieldValueChangedEvent);
|
||||
this.onFieldChanged(field);
|
||||
this.updateOptions([]);
|
||||
}
|
||||
|
||||
private isParentFormFieldEvent(event: FormFieldEvent): boolean {
|
||||
return event.field.id === this.getLinkedWidgetId();
|
||||
return event.field.id === this.linkedWidgetId;
|
||||
}
|
||||
|
||||
private isFormFieldEventOfTypeDropdown(event: FormFieldEvent): boolean {
|
||||
return event.field.type === FormFieldTypes.DROPDOWN;
|
||||
}
|
||||
|
||||
isLinkedWidget(): boolean {
|
||||
return !!this.getLinkedWidgetId();
|
||||
}
|
||||
|
||||
getLinkedWidgetId(): string {
|
||||
return this.field?.rule?.ruleOn;
|
||||
}
|
||||
|
||||
compareDropdownValues(opt1: FormFieldOption | string, opt2: FormFieldOption | string): boolean {
|
||||
if (!opt1 || !opt2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'string' && typeof opt2 === 'object') {
|
||||
return opt1 === opt2.id || opt1 === opt2.name;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'object' && typeof opt2 === 'string') {
|
||||
return opt1.id === opt2 || opt1.name === opt2;
|
||||
}
|
||||
|
||||
if (typeof opt1 === 'object' && typeof opt2 === 'object') {
|
||||
return opt1.id === opt2.id || opt1.name === opt2.name;
|
||||
}
|
||||
|
||||
return opt1 === opt2;
|
||||
}
|
||||
|
||||
getOptionValue(option: FormFieldOption, fieldValue: string): string | FormFieldOption {
|
||||
if (this.field.hasMultipleValues) {
|
||||
return option;
|
||||
private setOptionValue(option: FormFieldOption | FormFieldOption[], field: FormFieldModel) {
|
||||
if (Array.isArray(option) || field.hasMultipleValues) {
|
||||
field.value = option;
|
||||
return;
|
||||
}
|
||||
|
||||
let optionValue: string = '';
|
||||
if (option.id === DEFAULT_OPTION.id) {
|
||||
optionValue = undefined;
|
||||
} else if (option.name !== fieldValue) {
|
||||
} else if (option.name !== field.value) {
|
||||
optionValue = option.id;
|
||||
} else {
|
||||
optionValue = option.name;
|
||||
}
|
||||
return optionValue;
|
||||
}
|
||||
|
||||
private isValidRestConfig(): boolean {
|
||||
return this.isRestOptionType() && this.hasRestUrl();
|
||||
field.value = optionValue;
|
||||
}
|
||||
|
||||
private setPreviewState(): void {
|
||||
@ -397,51 +481,26 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
}
|
||||
|
||||
private isReadOnlyForm(): boolean {
|
||||
return !!this.field?.form?.readOnly;
|
||||
}
|
||||
|
||||
get isReadOnlyField(): boolean {
|
||||
return this.field.readOnly;
|
||||
}
|
||||
|
||||
private hasRestUrl(): boolean {
|
||||
return !!this.field?.restUrl;
|
||||
}
|
||||
|
||||
isReadOnlyType(): boolean {
|
||||
return this.field.type === 'readonly';
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
updateOptions(): void {
|
||||
if (this.isReadOnlyForm()) {
|
||||
this.list$ = of(this.field.options);
|
||||
private updateOptions(options?: FormFieldOption[]): void {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
this.field.options = options;
|
||||
}
|
||||
|
||||
this.list$.next(this.field.options);
|
||||
this.showInputFilter = this.field.options.length > this.appConfig.get<number>('form.dropDownFilterLimit', HIDE_FILTER_LIMIT);
|
||||
this.list$ = combineLatest([of(this.field.options), this.filter$]).pipe(
|
||||
map(([items, search]) => {
|
||||
if (!search) {
|
||||
return items;
|
||||
}
|
||||
return items.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()));
|
||||
}),
|
||||
takeUntil(this.onDestroy$)
|
||||
);
|
||||
}
|
||||
|
||||
resetRestApiErrorMessage() {
|
||||
private resetRestApiErrorMessage(): void {
|
||||
this.isRestApiFailed = false;
|
||||
this.restApiHostName = '';
|
||||
}
|
||||
|
||||
resetRestApiOptions() {
|
||||
this.field.options = [];
|
||||
private resetRestApiOptions(): void {
|
||||
this.updateOptions([]);
|
||||
this.resetValue();
|
||||
this.isRestApiFailed = true;
|
||||
this.restApiHostName = this.getRestUrlHostName();
|
||||
@ -454,17 +513,4 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
return this.field?.restUrl;
|
||||
}
|
||||
}
|
||||
|
||||
showRequiredMessage(): boolean {
|
||||
return (
|
||||
(this.isInvalidFieldRequired() || (this.isNoneValueSelected(this.field.value) && this.isRequired())) &&
|
||||
this.isTouched() &&
|
||||
!this.isRestApiFailed &&
|
||||
!this.variableOptionsFailed
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultOption(options: FormFieldOption[]): FormFieldOption {
|
||||
return options.find((option: FormFieldOption) => option.id === DEFAULT_OPTION.id);
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import {
|
||||
CONTENT_UPLOAD_DIRECTIVES,
|
||||
ContentNodeSelectorModule
|
||||
} from '@alfresco/adf-content-services';
|
||||
import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget';
|
||||
import { GroupCloudWidgetComponent } from './components/widgets/group/group-cloud.widget';
|
||||
import { PeopleCloudWidgetComponent } from './components/widgets/people/people-cloud.widget';
|
||||
import { AttachFileCloudWidgetComponent } from './components/widgets/attach-file/attach-file-cloud-widget.component';
|
||||
@ -73,7 +72,6 @@ import { FormCloudSpinnerService } from './services/spinner/form-cloud-spinner.s
|
||||
UploadCloudWidgetComponent,
|
||||
FormDefinitionSelectorCloudComponent,
|
||||
FormCustomOutcomesComponent,
|
||||
DropdownCloudWidgetComponent,
|
||||
RadioButtonsCloudWidgetComponent,
|
||||
AttachFileCloudWidgetComponent,
|
||||
PeopleCloudWidgetComponent,
|
||||
@ -90,7 +88,6 @@ import { FormCloudSpinnerService } from './services/spinner/form-cloud-spinner.s
|
||||
UploadCloudWidgetComponent,
|
||||
FormDefinitionSelectorCloudComponent,
|
||||
FormCustomOutcomesComponent,
|
||||
DropdownCloudWidgetComponent,
|
||||
RadioButtonsCloudWidgetComponent,
|
||||
AttachFileCloudWidgetComponent,
|
||||
PeopleCloudWidgetComponent,
|
||||
|
@ -15,7 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, Output, ViewEncapsulation, SimpleChanges, OnInit, OnDestroy, OnChanges, inject } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewEncapsulation,
|
||||
SimpleChanges,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
inject,
|
||||
ChangeDetectorRef
|
||||
} from '@angular/core';
|
||||
import {
|
||||
WidgetVisibilityService,
|
||||
FormService,
|
||||
@ -64,6 +76,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
|
||||
protected visibilityService = inject(WidgetVisibilityService);
|
||||
protected ecmModelService = inject(EcmModelService);
|
||||
protected nodeService = inject(NodesApiService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
/** Underlying form model instance. */
|
||||
@Input()
|
||||
@ -131,6 +144,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
|
||||
this.formService.validateForm.pipe(takeUntil(this.onDestroy$)).subscribe((validateFormEvent) => {
|
||||
if (validateFormEvent.errorsField.length > 0) {
|
||||
this.formError.next(validateFormEvent.errorsField);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,23 +1,16 @@
|
||||
<div class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
|
||||
<div
|
||||
class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="dropdownControl.invalid && dropdownControl.touched"
|
||||
[class.adf-readonly]="field.readOnly"
|
||||
>
|
||||
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
|
||||
<mat-form-field>
|
||||
<mat-select class="adf-select"
|
||||
[id]="field.id"
|
||||
[(ngModel)]="field.value"
|
||||
[disabled]="field.readOnly"
|
||||
(ngModelChange)="onFieldChanged(field)"
|
||||
(blur)="markAsTouched()">
|
||||
<mat-option *ngFor="let opt of field.options"
|
||||
[value]="getOptionValue(opt, field.value)"
|
||||
[id]="opt.id">{{opt.name}}
|
||||
</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</mat-option>
|
||||
<mat-select class="adf-select" [id]="field.id" [formControl]="dropdownControl">
|
||||
<mat-option *ngFor="let opt of field.options" [value]="opt" [id]="opt.id">{{opt.name}}</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="dropdownControl.disabled" [value]="field.value">{{field.value}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="!isReadOnlyField">
|
||||
<ng-container *ngIf="!isReadOnlyField && dropdownControl.touched">
|
||||
<error-widget [error]="field.validationSummary"></error-widget>
|
||||
<error-widget class="adf-dropdown-required-message" *ngIf="showRequiredMessage()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -139,9 +139,12 @@ describe('DropdownWidgetComponent', () => {
|
||||
describe('when is required', () => {
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'dropdown-id',
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
required: true
|
||||
});
|
||||
|
||||
widget.ngOnInit();
|
||||
});
|
||||
|
||||
it('should be able to display label with asterisk', async () => {
|
||||
@ -157,8 +160,9 @@ describe('DropdownWidgetComponent', () => {
|
||||
it('should be invalid if no default option after interaction', async () => {
|
||||
expect(element.querySelector('.adf-invalid')).toBeFalsy();
|
||||
|
||||
const dropdownSelect = element.querySelector('.adf-select');
|
||||
dropdownSelect.dispatchEvent(new Event('blur'));
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
|
||||
await dropdown.focus();
|
||||
await dropdown.blur();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
@ -186,12 +190,14 @@ describe('DropdownWidgetComponent', () => {
|
||||
id: 'dropdown-id',
|
||||
name: 'date-name',
|
||||
type: 'dropdown',
|
||||
readOnly: 'false',
|
||||
readOnly: false,
|
||||
restUrl: 'fake-rest-url',
|
||||
optionType: 'rest'
|
||||
});
|
||||
widget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
|
||||
widget.field.isVisible = true;
|
||||
widget.ngOnInit();
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@ -213,9 +219,8 @@ describe('DropdownWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const dropDownElement: any = element.querySelector('#dropdown-id');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
|
||||
expect(await dropdown.getValueText()).toBe('option_2');
|
||||
});
|
||||
|
||||
it('should select the empty value when no default is chosen', async () => {
|
||||
@ -224,8 +229,9 @@ describe('DropdownWidgetComponent', () => {
|
||||
|
||||
await (await loader.getHarness(MatSelectHarness)).open();
|
||||
|
||||
const dropDownElement: any = element.querySelector('#dropdown-id');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
|
||||
expect(await dropdown.getValueText()).toBe('Choose one...');
|
||||
expect(await widget.field.value).toBe('empty');
|
||||
});
|
||||
});
|
||||
|
||||
@ -237,7 +243,7 @@ describe('DropdownWidgetComponent', () => {
|
||||
id: 'dropdown-id',
|
||||
name: 'date-name',
|
||||
type: 'dropdown',
|
||||
readOnly: 'false',
|
||||
readOnly: false,
|
||||
restUrl: 'fake-rest-url',
|
||||
optionType: 'rest'
|
||||
});
|
||||
@ -264,9 +270,8 @@ describe('DropdownWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const dropDownElement: any = element.querySelector('#dropdown-id');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
|
||||
expect(await dropdown.getValueText()).toBe('option_2');
|
||||
});
|
||||
|
||||
it('should select the empty value when no default is chosen', async () => {
|
||||
@ -274,8 +279,9 @@ describe('DropdownWidgetComponent', () => {
|
||||
widget.ngOnInit();
|
||||
await (await loader.getHarness(MatSelectHarness)).open();
|
||||
|
||||
const dropDownElement: any = element.querySelector('#dropdown-id');
|
||||
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
|
||||
expect(await dropdown.getValueText()).toBe('Choose one...');
|
||||
expect(await widget.field.value).toBe('empty');
|
||||
});
|
||||
|
||||
it('should be disabled when the field is readonly', async () => {
|
||||
@ -283,9 +289,10 @@ describe('DropdownWidgetComponent', () => {
|
||||
id: 'dropdown-id',
|
||||
name: 'date-name',
|
||||
type: 'dropdown',
|
||||
readOnly: 'true',
|
||||
readOnly: true,
|
||||
restUrl: 'fake-rest-url'
|
||||
});
|
||||
widget.ngOnInit();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
@ -304,10 +311,13 @@ describe('DropdownWidgetComponent', () => {
|
||||
readOnly: true,
|
||||
params: { field: { name: 'date-name', type: 'dropdown' } }
|
||||
});
|
||||
widget.ngOnInit();
|
||||
|
||||
const select = await loader.getHarness(MatSelectHarness);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(await select.getValueText()).toEqual('FakeValue');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness);
|
||||
expect(await dropdown.getValueText()).toEqual('FakeValue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,20 +17,21 @@
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormService, FormFieldOption, WidgetComponent, ErrorWidgetComponent } from '@alfresco/adf-core';
|
||||
import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormService, FormFieldOption, WidgetComponent, ErrorWidgetComponent, ErrorMessageModel, FormFieldModel } from '@alfresco/adf-core';
|
||||
import { ProcessDefinitionService } from '../../services/process-definition.service';
|
||||
import { TaskFormService } from '../../services/task-form.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AbstractControl, FormControl, ReactiveFormsModule, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||
import { filter, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'dropdown-widget',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule, MatFormFieldModule, MatSelectModule, FormsModule, ErrorWidgetComponent],
|
||||
imports: [CommonModule, TranslateModule, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, ErrorWidgetComponent],
|
||||
templateUrl: './dropdown.widget.html',
|
||||
styleUrls: ['./dropdown.widget.scss'],
|
||||
host: {
|
||||
@ -46,19 +47,50 @@ import { FormsModule } from '@angular/forms';
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
|
||||
constructor(public formService: FormService, public taskFormService: TaskFormService, public processDefinitionService: ProcessDefinitionService) {
|
||||
super(formService);
|
||||
export class DropdownWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
|
||||
public formsService = inject(FormService);
|
||||
public taskFormService = inject(TaskFormService);
|
||||
public processDefinitionService = inject(ProcessDefinitionService);
|
||||
|
||||
dropdownControl = new FormControl<FormFieldOption | string>(undefined);
|
||||
|
||||
private readonly onDestroy$ = new Subject<void>();
|
||||
|
||||
get isReadOnlyType(): boolean {
|
||||
return this.field.type === 'readonly';
|
||||
}
|
||||
|
||||
get isReadOnlyField(): boolean {
|
||||
return this.field.readOnly;
|
||||
}
|
||||
|
||||
private get isRestType(): boolean {
|
||||
return this.field?.optionType === 'rest';
|
||||
}
|
||||
|
||||
private get hasRestUrl(): boolean {
|
||||
return !!this.field?.restUrl;
|
||||
}
|
||||
|
||||
private get isValidRestConfig(): boolean {
|
||||
return this.isRestType && this.hasRestUrl;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.isValidRestConfig() && !this.isReadOnlyForm()) {
|
||||
if (this.isValidRestConfig && !this.isReadOnlyForm()) {
|
||||
if (this.field.form.taskId) {
|
||||
this.getValuesByTaskId();
|
||||
} else {
|
||||
this.getValuesByProcessDefinitionId();
|
||||
}
|
||||
}
|
||||
|
||||
this.initFormControl();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy$.next();
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
getValuesByTaskId() {
|
||||
@ -85,41 +117,84 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getOptionValue(option: FormFieldOption, fieldValue: string): string {
|
||||
let optionValue: string = '';
|
||||
if (option.id === 'empty' || option.name !== fieldValue) {
|
||||
optionValue = option.id;
|
||||
} else {
|
||||
optionValue = option.name;
|
||||
}
|
||||
return optionValue;
|
||||
}
|
||||
|
||||
isReadOnlyType(): boolean {
|
||||
return this.field.type === 'readonly';
|
||||
}
|
||||
|
||||
showRequiredMessage(): boolean {
|
||||
return (this.isInvalidFieldRequired() || this.field.value === 'empty') && this.isTouched();
|
||||
}
|
||||
|
||||
get isReadOnlyField(): boolean {
|
||||
return this.field.readOnly;
|
||||
}
|
||||
|
||||
private isRestType(): boolean {
|
||||
return this.field?.optionType === 'rest';
|
||||
}
|
||||
|
||||
private isReadOnlyForm(): boolean {
|
||||
return !!this.field?.form?.readOnly;
|
||||
}
|
||||
|
||||
private hasRestUrl(): boolean {
|
||||
return !!this.field?.restUrl;
|
||||
private initFormControl() {
|
||||
if (this.field?.required) {
|
||||
this.dropdownControl.addValidators([this.customRequiredValidator(this.field)]);
|
||||
}
|
||||
|
||||
if (this.field?.readOnly || this.readOnly) {
|
||||
this.dropdownControl.disable({ emitEvent: false });
|
||||
}
|
||||
|
||||
this.dropdownControl.valueChanges
|
||||
.pipe(
|
||||
filter(() => !!this.field),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this.setOptionValue(value, this.field);
|
||||
this.handleErrors();
|
||||
this.onFieldChanged(this.field);
|
||||
});
|
||||
|
||||
this.dropdownControl.setValue(this.getOptionValue(this.field?.value), { emitEvent: false });
|
||||
this.handleErrors();
|
||||
}
|
||||
|
||||
private isValidRestConfig(): boolean {
|
||||
return this.isRestType() && this.hasRestUrl();
|
||||
private handleErrors() {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dropdownControl.valid) {
|
||||
this.field.validationSummary = new ErrorMessageModel('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dropdownControl.invalid && this.dropdownControl.errors.required) {
|
||||
this.field.validationSummary = new ErrorMessageModel({ message: 'FORM.FIELD.REQUIRED' });
|
||||
}
|
||||
}
|
||||
|
||||
private setOptionValue(option: string | FormFieldOption, field: FormFieldModel) {
|
||||
if (typeof option === 'string') {
|
||||
field.value = option;
|
||||
return;
|
||||
}
|
||||
if (option.id === 'empty' || option.name !== field.value) {
|
||||
field.value = option.id;
|
||||
return;
|
||||
}
|
||||
|
||||
field.value = option.name;
|
||||
}
|
||||
|
||||
private getOptionValue(value?: string | FormFieldOption) {
|
||||
if (this.field?.readOnly || this.readOnly) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return this.field.options.find((option) => option.id === value || option.name === value);
|
||||
}
|
||||
|
||||
return value as FormFieldOption | undefined;
|
||||
}
|
||||
|
||||
private customRequiredValidator(field: FormFieldModel): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const isEmptyInputValue = (value: any) => value == null || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0);
|
||||
const isEqualToEmptyValue = (value: any) =>
|
||||
field.hasEmptyValue &&
|
||||
(value === field.emptyOption.id ||
|
||||
value === field.emptyOption.name ||
|
||||
(value.id === field.emptyOption.id && value.name === field.emptyOption.name));
|
||||
|
||||
return isEmptyInputValue(control.value) || isEqualToEmptyValue(control.value) ? { required: true } : null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user