diff --git a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.spec.ts index 0aab40007a..bd76988eac 100644 --- a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.spec.ts +++ b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.spec.ts @@ -367,6 +367,33 @@ describe('PropertyGroupTranslatorService', () => { expect(cardViewProperty.value).toBe('two'); }); + it('should translate property for type LIST constraint with multivalued property', () => { + const definition: Definition = { + properties: [ + { + id: 'FAS:PLAGUE', + constraints: [ + { + type: 'LIST', + parameters: { + allowedValues: ['one', 'two', 'three'] + } + } + ] + } as Constraint + ] + }; + property.dataType = 'd:text'; + property.multiValued = true; + propertyValues = { 'FAS:PLAGUE': ['one', 'three'] }; + const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues, definition); + + const cardViewProperty = cardViewGroup[0].properties[0] as CardViewSelectItemModel; + expect(cardViewProperty instanceof CardViewSelectItemModel).toBeTruthy(); + expect(cardViewProperty.multivalued).toBe(true); + expect(cardViewProperty.value).toEqual(['one', 'three']); + }); + it('should translate content type properties into card items', () => { const propertyBase = { id: 'fk:brendonstare', diff --git a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts index 52651254c7..1171efd8f0 100644 --- a/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/property-groups-translator.service.ts @@ -120,7 +120,7 @@ export class PropertyGroupTranslatorService { if (this.isListOfValues(propertyDefinition.constraints)) { const options = propertyDefinition.constraints[0].parameters.allowedValues.map((value) => ({ key: value, label: value })); - const properties = Object.assign(propertyDefinition, { options$: of(options) }); + const properties = Object.assign(propertyDefinition, { options$: of(options), multivalued: isMultiValued }); cardViewItemProperty = new CardViewSelectItemModel(properties); } else { diff --git a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html index 639296d070..ac7780208e 100644 --- a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html +++ b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html @@ -13,7 +13,8 @@ >{{ property.label | translate }} - {{ 'CORE.CARDVIEW.NONE' | translate }} + {{ 'CORE.CARDVIEW.NONE' | translate }} {{ option.label | translate }} @@ -50,24 +51,60 @@ > {{ property.label | translate }} - - + @if (property.multivalued) { + + @for (val of property.value; track $index) { + + {{getOptionLabel(val) | async}} + @if (isEditable) { + + } + + } + + + } @else { + + } + diff --git a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.spec.ts b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.spec.ts index 1e14671d55..264969aed4 100644 --- a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.spec.ts +++ b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.spec.ts @@ -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', () => { diff --git a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.ts b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.ts index 99193fdd18..78ff4feb5b 100644 --- a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.ts @@ -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> implements OnInit, OnChanges { static HIDE_FILTER_LIMIT = 5; + readonly separatorKeysCodes = [ENTER] as const; @Input() options$: Observable[]>; @@ -63,13 +69,13 @@ export class CardViewSelectItemComponent extends BaseCardView(''); showInputFilter: boolean = false; list$: Observable[]> = null; templateType = ''; autocompleteControl = new UntypedFormControl(); - editedValue: string | number; + editedValue = ''; + filteredOptions: CardViewSelectItemOption[] = []; private readonly destroyRef = inject(DestroyRef); private readonly appConfig = inject(AppConfigService); @@ -81,17 +87,21 @@ export class CardViewSelectItemComponent extends BaseCardView 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[]> { @@ -134,32 +144,75 @@ export class CardViewSelectItemComponent extends BaseCardView { - const selectedOption = options.find((option) => option.key === event.option.value); - if (selectedOption) { - this.autocompleteControl.setValue(selectedOption.label); - this.cardViewUpdateService.update({ ...this.property } as CardViewSelectItemModel, 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, 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('
'); } + 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 { + return this.getOptions().pipe( + take(1), + map((options) => options.find((option) => option.key === value)?.label) + ); + } + private get optionsLimit(): number { return this.appConfig.get('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[]) => { + this.filteredOptions = options; + }); + } } diff --git a/lib/core/src/lib/card-view/interfaces/card-view-selectitem-properties.interface.ts b/lib/core/src/lib/card-view/interfaces/card-view-selectitem-properties.interface.ts index d475d1234c..c90b0ca5fd 100644 --- a/lib/core/src/lib/card-view/interfaces/card-view-selectitem-properties.interface.ts +++ b/lib/core/src/lib/card-view/interfaces/card-view-selectitem-properties.interface.ts @@ -24,7 +24,7 @@ export interface CardViewSelectItemOption { } export interface CardViewSelectItemProperties extends CardViewItemProperties { - value: string | number; + value: T | T[]; options$: Observable[]>; displayNoneOption?: boolean; autocompleteBased?: boolean; diff --git a/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts b/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts index cdd5ff4284..ed927b9df5 100644 --- a/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts +++ b/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts @@ -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(); + }); + }); }); }); diff --git a/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts b/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts index eaf507a315..b8b30ca6ea 100644 --- a/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts @@ -40,8 +40,10 @@ export class CardViewSelectItemModel 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 ?? ''); }) ); } diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index 3673781bb0..af8cd80ebf 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -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", diff --git a/lib/core/src/lib/testing/unit-testing-utils.ts b/lib/core/src/lib/testing/unit-testing-utils.ts index df6417157e..310f7ac8ac 100644 --- a/lib/core/src/lib/testing/unit-testing-utils.ts +++ b/lib/core/src/lib/testing/unit-testing-utils.ts @@ -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 { + return this.loader.getHarness(MatChipInputHarness); + } + + async getMatChipInputByDataAutomationId(dataAutomationId: string): Promise { + return this.loader.getHarness(MatChipInputHarness.with({ selector: `[data-automation-id="${dataAutomationId}"]` })); + } + + async getMatChipInputByCSS(selector: string): Promise { + return this.loader.getHarness(MatChipInputHarness.with({ selector })); + } + /** MatFromField related methods */ async getMatFormField(): Promise {