Skip to content

Commit

Permalink
AAE-23521 Fix improve dropdown reactive form (#10040)
Browse files Browse the repository at this point in the history
* AAE-23521 Fix improve dropdown reactive form

* AAE-23521 Update process services dropdown

* AAE-23521 pr suggestions

* AAE-23521 move tests

* AAE-23521 fixes
  • Loading branch information
kathrine0 authored Aug 7, 2024
1 parent 5930a27 commit f88ff10
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 321 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.adf-error {
display: flex;
align-items: center;

&-widget-container {
height: auto;
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import 'styles/mat-selectors';

.adf {
&-dropdown-widget {
width: 100%;
Expand Down Expand Up @@ -25,5 +27,9 @@
&-dropdown-failed-message .adf-error-container {
margin-top: 1px;
}

#{$mat-select-arrow-wrapper} {
height: auto;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -95,15 +94,15 @@ 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 () => {
widget.field.value = 'empty';
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 () => {
Expand Down Expand Up @@ -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');
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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)', () => {
Expand Down
Loading

0 comments on commit f88ff10

Please sign in to comment.