[MNT-25149] Card view select item multivalued property support (#11891)

* [MNT-25149] Card view select item multivalued property support

* [MNT-25149] CR fixes

* [MNT-25149] CR fixes

* [MNT-25149] CR fixes
This commit is contained in:
Michal Kinas
2026-05-20 11:39:42 +02:00
committed by GitHub
parent 1d163c6749
commit ece50ac32c
10 changed files with 519 additions and 54 deletions
@@ -13,7 +13,8 @@
>{{ property.label | translate }}
</mat-label>
<mat-select
[(ngModel)]="value"
[multiple]="property.multivalued"
[(ngModel)]="property.value"
[disabled]="isReadonlyProperty || !editable"
[ngClass]="{ 'adf-property-readonly-value': isReadonlyProperty || !editable }"
panelClass="adf-select-filter"
@@ -25,7 +26,7 @@
(validated)="onValidation($event)"
>
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)" />
<mat-option *ngIf="displayNoneOption">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option *ngIf="displayNoneOption && !property.multivalued">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option *ngFor="let option of list$ | async" [value]="option.key">
{{ option.label | translate }}
</mat-option>
@@ -50,24 +51,60 @@
>
{{ property.label | translate }}
</mat-label>
<input
matInput
[matAutocomplete]="auto"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty || !editable
}"
title="{{ property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="autocompleteControl"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-value-' + property.key"
/>
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="onOptionSelected($event)">
@if (property.multivalued) {
<mat-chip-grid #chipGrid [attr.aria-label]="'CORE.CARDVIEW.SELECTED_VALUES' | translate">
@for (val of property.value; track $index) {
<mat-chip-row (removed)="removeChip(val)">
{{getOptionLabel(val) | async}}
@if (isEditable) {
<button matChipRemove [attr.aria-label]="'CORE.CARDVIEW.REMOVE' | translate: { value: getOptionLabel(val) | async }">
<mat-icon
adf-icon="close"
matChipRemove
/>
</button>
}
</mat-chip-row>
}
</mat-chip-grid>
<input
matInput
[matAutocomplete]="auto"
[matChipInputFor]="chipGrid"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addValueToList($event)"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty || !editable
}"
title="{{ property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="autocompleteControl"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-value-' + property.key"
/>
} @else {
<input
matInput
[matAutocomplete]="auto"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty || !editable
}"
title="{{ property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="autocompleteControl"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-value-' + property.key"
/>
}
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onOptionSelected($event)">
<mat-option
*ngFor="let option of property.options$ | async"
*ngFor="let option of filteredOptions"
[value]="option.key"
[attr.data-automation-id]="'card-autocomplete-based-selectitem-option-' + property.key"
>
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { CardViewSelectItemModel } from '../../models/card-view-selectitem.model';
import { CardViewSelectItemComponent } from './card-view-selectitem.component';
import { of } from 'rxjs';
@@ -25,7 +25,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UnitTestingUtils } from '../../../testing/unit-testing-utils';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { DebugElement } from '@angular/core';
import { DebugElement, SimpleChange, SimpleChanges } from '@angular/core';
import { CardViewPropertyValidatorDirective } from '../../directives/card-view-property-validator.directive';
import { MatError } from '@angular/material/form-field';
import { FormControl, NgModel } from '@angular/forms';
@@ -114,14 +114,14 @@ describe('CardViewSelectItemComponent', () => {
component.ngOnChanges({});
fixture.detectChanges();
expect(component.value).toEqual('two');
expect(component.property.value).toEqual('two');
expect(component.isEditable).toBe(true);
const options = await testingUtils.getMatSelectOptions();
expect(options.length).toEqual(4);
await options[1].click();
expect(component.value).toEqual('one');
expect(component.property.value).toEqual('one');
});
it('should be possible edit selectBox item with numbers', async () => {
@@ -134,7 +134,7 @@ describe('CardViewSelectItemComponent', () => {
component.ngOnChanges({});
fixture.detectChanges();
expect(component.value).toEqual(2);
expect(component.property.value).toEqual(2);
expect(component.isEditable).toBe(true);
const options = await testingUtils.getMatSelectOptions();
@@ -142,7 +142,7 @@ describe('CardViewSelectItemComponent', () => {
expect(options.length).toEqual(4);
await options[1].click();
expect(component.value).toEqual(1);
expect(component.property.value).toEqual(1);
});
it('should be able to enable None option', async () => {
@@ -174,11 +174,21 @@ describe('CardViewSelectItemComponent', () => {
component.ngOnChanges({});
component.editable = true;
fixture.detectChanges();
const not_editable_label = fixture.nativeElement.querySelector('.adf-property-label-not-editable');
const not_editable_label = testingUtils.getByCSS('.adf-property-label-not-editable');
const field = await testingUtils.getMatFormFieldByCSS('.adf-property-value');
expect(await field.hasLabel()).toBeTrue();
expect(not_editable_label).toBeNull();
expect(not_editable_label).toBeFalsy();
});
it('should have proper aria-label on select box', async () => {
component.ngOnChanges({});
component.editable = true;
fixture.detectChanges();
const selectBox = await testingUtils.getMatSelectByDataAutomationId('select-box');
const host = await selectBox.host();
expect(await host.getAttribute('aria-label')).toBe('Select box label');
});
});
@@ -276,7 +286,7 @@ describe('CardViewSelectItemComponent', () => {
const autocompleteValueSpy = spyOn(cardViewUpdateService.autocompleteInputValue$, 'next');
component.editedValue = '';
component.editable = true;
component.ngOnChanges({ property: { firstChange: true } } as any);
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
component.autocompleteControl.setValue('new value');
@@ -288,7 +298,11 @@ describe('CardViewSelectItemComponent', () => {
it('should update value correctly on option selected', () => {
cardViewUpdateService.update = jasmine.createSpy('update');
const event: MatAutocompleteSelectedEvent = {
component.filteredOptions = [
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' }
];
const event = {
option: {
value: '1'
}
@@ -300,7 +314,7 @@ describe('CardViewSelectItemComponent', () => {
component.onOptionSelected(event);
fixture.detectChanges();
expect(component.autocompleteControl.value).toBe('Option 1');
expect(component.property.value).toBe('1');
expect(cardViewUpdateService.update).toHaveBeenCalledWith(jasmine.objectContaining(component.property), '1');
});
@@ -319,6 +333,10 @@ describe('CardViewSelectItemComponent', () => {
});
it('should populate options for autocomplete', async () => {
component.filteredOptions = [
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' }
];
component.ngOnChanges({});
fixture.detectChanges();
@@ -327,6 +345,288 @@ describe('CardViewSelectItemComponent', () => {
expect(await options[0].getText()).toContain('Option 1');
expect(await options[1].getText()).toContain('Option 2');
});
it('should update filteredOptions when autocompleteControl value changes', fakeAsync(() => {
component.editedValue = '';
component.editable = true;
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
component.autocompleteControl.setValue('Option 1');
fixture.detectChanges();
tick(50);
expect(component.filteredOptions.length).toBe(1);
expect(component.filteredOptions[0].label).toBe('Option 1');
component.autocompleteControl.setValue('2');
fixture.detectChanges();
tick(50);
expect(component.filteredOptions.length).toBe(1);
expect(component.filteredOptions[0].label).toBe('Option 2');
}));
it('should render autocomplete options from filteredOptions array', async () => {
component.filteredOptions = [
{ key: '1', label: 'Option 1' },
{ key: '2', label: 'Option 2' }
];
component.ngOnChanges({});
fixture.detectChanges();
const options = await testingUtils.typeAndGetOptionsForMatAutoComplete(fixture, 'Option');
expect(options.length).toBe(2);
expect(await options[0].getText()).toContain('Option 1');
expect(await options[1].getText()).toContain('Option 2');
});
it('should have proper aria-label on autocomplete input', () => {
component.ngOnChanges({});
fixture.detectChanges();
const input = testingUtils.getInputByCSS('.adf-property-value');
expect(input).toBeTruthy();
expect(input.getAttribute('aria-label')).toBe('Test Label');
});
});
describe('Multivalued select', () => {
const multivaluedMockData = [
{ key: 'one', label: 'One' },
{ key: 'two', label: 'Two' },
{ key: 'three', label: 'Three' }
];
beforeEach(() => {
component.property = new CardViewSelectItemModel({
label: 'Multi Select Label',
value: ['one'],
key: 'multi-key',
editable: true,
multivalued: true,
options$: of(multivaluedMockData)
});
});
it('should initialize property.value as empty array if not set and multivalued is true', () => {
component.property.value = null;
component.ngOnChanges({});
fixture.detectChanges();
expect(component.property.value).toEqual([]);
});
it('should enable multiple selection on select box when multivalued is true', async () => {
component.editable = true;
component.displayNoneOption = false;
component.ngOnChanges({});
fixture.detectChanges();
const selectBox = await testingUtils.getMatSelectByDataAutomationId('select-box');
expect(await selectBox.isMultiple()).toBe(true);
});
it('should not display None option when multivalued is true', async () => {
component.editable = true;
component.displayNoneOption = true;
component.ngOnChanges({});
fixture.detectChanges();
const options = await testingUtils.getMatSelectOptions();
const optionTexts = await Promise.all(options.map((opt) => opt.getText()));
const hasNoneOption = optionTexts.some((text) => text.includes('CORE.CARDVIEW.NONE'));
expect(hasNoneOption).toBe(false);
});
it('should allow selecting multiple values', async () => {
component.editable = true;
component.displayNoneOption = false;
component.ngOnChanges({});
fixture.detectChanges();
expect(component.property.value).toEqual(['one']);
const options = await testingUtils.getMatSelectOptions();
await options[1].click();
expect(component.property.value).toContain('two');
expect(component.property.value.length).toBe(2);
});
});
describe('Multivalued autocomplete based', () => {
const multivaluedOptions = [
{ key: 'option1', label: 'Option 1' },
{ key: 'option2', label: 'Option 2' },
{ key: 'option3', label: 'Option 3' }
];
beforeEach(() => {
component.property = new CardViewSelectItemModel({
label: 'Multi Autocomplete Label',
value: [],
key: 'multi-autocomplete-key',
editable: true,
autocompleteBased: true,
multivalued: true,
options$: of(multivaluedOptions)
});
});
it('should add value to array when option is selected', async () => {
component.editable = true;
component.filteredOptions = multivaluedOptions;
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
const options = await testingUtils.typeAndGetOptionsForMatAutoComplete(fixture, 'Option');
expect(options.length).toBe(3);
await options[0].click();
expect(component.property.value).toContain('option1');
});
it('should render chip remove buttons with proper aria-label', () => {
component.property.value = ['option1'];
component.editable = true;
component.ngOnChanges({});
fixture.detectChanges();
const removeButtons = testingUtils.getAllByCSS('button[aria-label*="CORE.CARDVIEW.REMOVE"]');
expect(removeButtons.length).toBeGreaterThan(0);
expect(removeButtons[0].nativeElement.getAttribute('aria-label')).toContain('CORE.CARDVIEW.REMOVE');
});
it('should remove chip when remove button is clicked', async () => {
const updateSpy = spyOn(cardViewUpdateService, 'update');
component.property.value = ['option1', 'option2'];
component.editable = true;
component.ngOnChanges({});
fixture.detectChanges();
const removeButtons = testingUtils.getAllByCSS('button[matChipRemove]');
expect(removeButtons.length).toBe(2);
removeButtons[0].nativeElement.click();
fixture.detectChanges();
expect(component.property.value).toEqual(['option2']);
expect(updateSpy).toHaveBeenCalledWith(jasmine.objectContaining(component.property), ['option2']);
const remainingChips = await testingUtils.getMatChips();
expect(remainingChips.length).toBe(1);
});
it('should add value to list when pressing ENTER with valid option', fakeAsync(() => {
component.property.value = ['option1'];
component.filteredOptions = [
{ key: 'option2', label: 'option2' },
{ key: 'option3', label: 'option3' }
];
component.editable = true;
component.ngOnInit();
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
tick(50);
const updateSpy = spyOn(cardViewUpdateService, 'update');
const chipInputEvent = { value: 'option2', chipInput: { clear: jasmine.createSpy('clear') } } as any;
component.addValueToList(chipInputEvent);
fixture.detectChanges();
tick(50);
expect(component.property.value).toContain('option2');
expect(updateSpy).toHaveBeenCalledWith(jasmine.objectContaining(component.property), ['option1', 'option2']);
expect(chipInputEvent.chipInput.clear).toHaveBeenCalled();
}));
it('should not add value when pressing ENTER with invalid option', fakeAsync(() => {
component.property.value = ['option1'];
component.filteredOptions = [
{ key: 'option2', label: 'option2' },
{ key: 'option3', label: 'option3' }
];
component.editable = true;
component.ngOnInit();
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
tick(50);
const updateSpy = spyOn(cardViewUpdateService, 'update');
const chipInputEvent = { value: 'invalidOption', chipInput: { clear: jasmine.createSpy('clear') } } as any;
component.addValueToList(chipInputEvent);
fixture.detectChanges();
tick(50);
expect(component.property.value).toEqual(['option1']);
expect(updateSpy).not.toHaveBeenCalled();
}));
it('should filter out already selected options from filteredOptions', fakeAsync(() => {
component.filteredOptions = [...multivaluedOptions];
component.property.value = ['option1'];
component.editedValue = '';
component.editable = true;
component.ngOnInit();
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
component.autocompleteControl.setValue('Op');
fixture.detectChanges();
tick(50);
expect(component.filteredOptions.length).toBe(2);
expect(component.filteredOptions[0].label).toBe('Option 2');
expect(component.filteredOptions[1].label).toBe('Option 3');
}));
it('should update filteredOptions after option selection', fakeAsync(() => {
component.filteredOptions = [...multivaluedOptions];
component.editable = true;
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
const event = {
option: {
value: 'option1'
}
} as MatAutocompleteSelectedEvent;
component.onOptionSelected(event);
tick(50);
expect(component.property.value).toContain('option1');
expect(component.filteredOptions.length).toBe(2);
expect(component.filteredOptions[0].label).toBe('Option 2');
expect(component.filteredOptions[1].label).toBe('Option 3');
}));
it('should update filteredOptions after chip removal', fakeAsync(() => {
component.property.value = ['option1', 'option2'];
component.filteredOptions = [multivaluedOptions[2]];
component.editable = true;
component.ngOnChanges({ property: new SimpleChange(null, component.property, true) } as SimpleChanges);
fixture.detectChanges();
component.removeChip('option1');
tick(100);
expect(component.property.value).not.toContain('option1');
expect(component.filteredOptions.length).toBe(2);
expect(component.filteredOptions[0].label).toBe('Option 1');
expect(component.filteredOptions[1].label).toBe('Option 3');
}));
});
describe('Validation', () => {
@@ -22,7 +22,7 @@ import { CardViewSelectItemOption } from '../../interfaces/card-view.interfaces'
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
import { BaseCardView } from '../base-card-view';
import { AppConfigService } from '../../../app-config/app-config.service';
import { map, debounceTime, filter, first } from 'rxjs/operators';
import { map, debounceTime, filter, take } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -32,6 +32,9 @@ import { MatInputModule } from '@angular/material/input';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CardViewPropertyValidatorDirective } from '../../directives/card-view-property-validator.directive';
import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
import { ENTER } from '@angular/cdk/keycodes';
import { IconModule } from '../../../icon/icon.module';
@Component({
selector: 'adf-card-view-selectitem',
@@ -45,7 +48,9 @@ import { CardViewPropertyValidatorDirective } from '../../directives/card-view-p
MatInputModule,
ReactiveFormsModule,
CardViewPropertyValidatorDirective,
FormsModule
FormsModule,
MatChipsModule,
IconModule
],
templateUrl: './card-view-selectitem.component.html',
styleUrls: ['./card-view-selectitem.component.scss'],
@@ -54,6 +59,7 @@ import { CardViewPropertyValidatorDirective } from '../../directives/card-view-p
})
export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItemModel<string | number>> implements OnInit, OnChanges {
static HIDE_FILTER_LIMIT = 5;
readonly separatorKeysCodes = [ENTER] as const;
@Input() options$: Observable<CardViewSelectItemOption<string | number>[]>;
@@ -63,13 +69,13 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
@Input()
displayEmpty: boolean = true;
value: string | number;
filter$ = new BehaviorSubject<string>('');
showInputFilter: boolean = false;
list$: Observable<CardViewSelectItemOption<string | number>[]> = null;
templateType = '';
autocompleteControl = new UntypedFormControl();
editedValue: string | number;
editedValue = '';
filteredOptions: CardViewSelectItemOption<string | number>[] = [];
private readonly destroyRef = inject(DestroyRef);
private readonly appConfig = inject(AppConfigService);
@@ -81,17 +87,21 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
}
ngOnChanges(changes: SimpleChanges): void {
this.value = this.property.value;
if (!this.property.value && this.property.multivalued) {
this.property.value = [];
}
if (changes.property?.firstChange) {
this.autocompleteControl.valueChanges
.pipe(
filter((textInputValue) => textInputValue !== this.editedValue && textInputValue !== null),
filter((textInputValue) => textInputValue !== this.editedValue && textInputValue !== null && !Array.isArray(textInputValue)),
debounceTime(50),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((textInputValue) => {
this.editedValue = textInputValue;
this.cardViewUpdateService.autocompleteInputValue$.next(textInputValue);
this.filterOptions();
});
}
@@ -120,7 +130,7 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
}
onFilterInputChange(value: string) {
this.filter$.next(value.toString());
this.filter$.next(value);
}
private getOptions(): Observable<CardViewSelectItemOption<string | number>[]> {
@@ -134,32 +144,75 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
}
onOptionSelected(event: MatAutocompleteSelectedEvent) {
this.getOptions()
.pipe(first())
.subscribe((options) => {
const selectedOption = options.find((option) => option.key === event.option.value);
if (selectedOption) {
this.autocompleteControl.setValue(selectedOption.label);
this.cardViewUpdateService.update({ ...this.property } as CardViewSelectItemModel<string>, selectedOption.key);
}
});
const selectedOption = this.filteredOptions.find((option) => option.key === event.option.value);
if (selectedOption) {
if (this.property.multivalued) {
this.property.value.push(event.option.value);
} else {
this.property.value = event.option.value;
this.autocompleteControl.setValue(selectedOption.label);
}
this.cardViewUpdateService.update(this.property, this.property.value);
this.filterOptions();
}
}
onChange(event: MatSelectChange): void {
const selectedOption = event.value !== undefined ? event.value : null;
this.cardViewUpdateService.update({ ...this.property } as CardViewSelectItemModel<string>, selectedOption);
this.property.value = selectedOption;
const selectedOptions = event.value !== undefined ? event.value : null;
this.cardViewUpdateService.update(this.property, selectedOptions);
this.property.value = selectedOptions;
}
onValidation(errors: string[]): void {
this._error = errors.join('<br>');
}
removeChip(value: string | number) {
this.property.value = this.property.value.filter((v) => v !== value);
this.cardViewUpdateService.update(this.property, this.property.value);
this.filterOptions();
}
addValueToList(newListItem: MatChipInputEvent) {
const selectedOption = this.filteredOptions.find((option) => option.key === newListItem.value || option.label === newListItem.value);
if (selectedOption) {
this.property.value.push(selectedOption.key);
this.cardViewUpdateService.update(this.property, this.property.value);
newListItem.chipInput.clear();
this.filterOptions();
}
}
get showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
getOptionLabel(value: string | number): Observable<string> {
return this.getOptions().pipe(
take(1),
map((options) => options.find((option) => option.key === value)?.label)
);
}
private get optionsLimit(): number {
return this.appConfig.get<number>('content-metadata.selectFilterLimit', CardViewSelectItemComponent.HIDE_FILTER_LIMIT);
}
private filterOptions() {
this.getOptions()
.pipe(
map((options) =>
options.filter((option) => {
const isSelected = this.property.multivalued
? this.property.value.some((val) => val === option.key)
: this.property.value === option.key;
return !isSelected && option.label.toLowerCase().includes(this.editedValue.toLowerCase());
})
)
)
.pipe(take(1))
.subscribe((options: CardViewSelectItemOption<string | number>[]) => {
this.filteredOptions = options;
});
}
}
@@ -24,7 +24,7 @@ export interface CardViewSelectItemOption<T> {
}
export interface CardViewSelectItemProperties<T> extends CardViewItemProperties {
value: string | number;
value: T | T[];
options$: Observable<CardViewSelectItemOption<T>[]>;
displayNoneOption?: boolean;
autocompleteBased?: boolean;
@@ -72,5 +72,35 @@ describe('CardViewSelectItemModel', () => {
const itemModel = new CardViewSelectItemModel(properties);
expect(itemModel.autocompleteBased).toBe(true);
}));
it('should return comma-separated labels for multivalued array', (done) => {
properties.value = ['one', 'three'];
const itemModel = new CardViewSelectItemModel(properties);
itemModel.displayValue.subscribe((value) => {
expect(value).toBe('One, Three');
done();
});
});
it('should return empty string for multivalued array with no matching options', (done) => {
properties.value = ['nonexistent'];
const itemModel = new CardViewSelectItemModel(properties);
itemModel.displayValue.subscribe((value) => {
expect(value).toBe('');
done();
});
});
it('should handle empty multivalued array', (done) => {
properties.value = [];
const itemModel = new CardViewSelectItemModel(properties);
itemModel.displayValue.subscribe((value) => {
expect(value).toBe('');
done();
});
});
});
});
@@ -40,8 +40,10 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
this.valueFetch$ = this.options$.pipe(
switchMap((options) => {
const option = options.find((o) => o.key === this.value?.toString());
return of(option ? option.label : '');
if (Array.isArray(this.value)) {
return of(this.value.map((v) => options.find((o) => o.key === v)?.label).join(', '));
}
return of(options.find((o) => o.key === this.value?.toString())?.label ?? '');
})
);
}
+2
View File
@@ -255,6 +255,8 @@
"VALUE": "Value"
},
"NONE": "None",
"SELECTED_VALUES": "Selected values",
"REMOVE": "Remove {{ value }}",
"VALIDATORS": {
"FLOAT_VALIDATION_ERROR": "Use a number format",
"INT_VALIDATION_ERROR": "Use an integer format",
+15 -1
View File
@@ -20,7 +20,7 @@ import { DebugElement, Type } from '@angular/core';
import { By } from '@angular/platform-browser';
import { MatSelectHarness } from '@angular/material/select/testing';
import { MatOptionHarness } from '@angular/material/core/testing';
import { MatChipGridHarness, MatChipHarness, MatChipListboxHarness } from '@angular/material/chips/testing';
import { MatChipGridHarness, MatChipHarness, MatChipInputHarness, MatChipListboxHarness } from '@angular/material/chips/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatIconHarness } from '@angular/material/icon/testing';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
@@ -344,6 +344,20 @@ export class UnitTestingUtils {
return this.loader.hasHarness(MatChipGridHarness);
}
/** MatChipInput related methods */
async getMatChipInput(): Promise<MatChipInputHarness> {
return this.loader.getHarness(MatChipInputHarness);
}
async getMatChipInputByDataAutomationId(dataAutomationId: string): Promise<MatChipInputHarness> {
return this.loader.getHarness(MatChipInputHarness.with({ selector: `[data-automation-id="${dataAutomationId}"]` }));
}
async getMatChipInputByCSS(selector: string): Promise<MatChipInputHarness> {
return this.loader.getHarness(MatChipInputHarness.with({ selector }));
}
/** MatFromField related methods */
async getMatFormField(): Promise<MatFormFieldHarness> {