mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[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:
+56
-19
@@ -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"
|
||||
>
|
||||
|
||||
+311
-11
@@ -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', () => {
|
||||
|
||||
+72
-19
@@ -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 ?? '');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user