mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACS-4986] Advanced Search - New component for Tags and Location filters (#8655)
* [ACS-4986] SearchChipsAutocompleteComponent - minimal state * [ACS-4986] refactored components * [ACS-4986] documentation * [ACS-4986] i18n * [ACS-4986] versionIndex.md * [ACS-4986] unit tests * [ACS-4986] replaced function calls on pipe * [ACS-4986] linting * [ACS-4986] slight correction * [ACS-4986] missing types * [ACS-4986] space * [ACS-4986] moved pipe + docs & tests * [ACS-4986] changed pipe type * [ACS-4986] versionIndex.md * [ACS-4986] removed 'important' in styles * [ACS-4986] fixed code smell * [ACS-4986] linting
This commit is contained in:
committed by
GitHub
parent
5fafb0ea6f
commit
61d5aa965b
@@ -297,7 +297,8 @@
|
||||
"APPLY": "Apply",
|
||||
"CLEAR-ALL": "Clear all",
|
||||
"SHOW-MORE": "Show more",
|
||||
"SHOW-LESS": "Show less"
|
||||
"SHOW-LESS": "Show less",
|
||||
"ADD_OPTION": "Add Option..."
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CLOSE": "Close",
|
||||
@@ -331,7 +332,8 @@
|
||||
"BEYOND-MAX-DATETIME": "The datetime is beyond the maximum datetime."
|
||||
},
|
||||
"ARIA-LABEL": {
|
||||
"SEARCH_FILTER": "Search Filter List"
|
||||
"SEARCH_FILTER": "Search Filter List",
|
||||
"OPTIONS-SELECTION": "Options Selection"
|
||||
},
|
||||
"ANY": "Any"
|
||||
},
|
||||
|
@@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NodeNameTooltipPipe } from './node-name-tooltip.pipe';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { IsIncludedPipe } from './is-included.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -26,13 +27,16 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [
|
||||
NodeNameTooltipPipe
|
||||
NodeNameTooltipPipe,
|
||||
IsIncludedPipe
|
||||
],
|
||||
providers: [
|
||||
NodeNameTooltipPipe
|
||||
NodeNameTooltipPipe,
|
||||
IsIncludedPipe
|
||||
],
|
||||
exports: [
|
||||
NodeNameTooltipPipe
|
||||
NodeNameTooltipPipe,
|
||||
IsIncludedPipe
|
||||
]
|
||||
})
|
||||
export class ContentPipeModule {
|
||||
|
44
lib/content-services/src/lib/pipes/is-included.pipe.spec.ts
Normal file
44
lib/content-services/src/lib/pipes/is-included.pipe.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IsIncludedPipe } from './is-included.pipe';
|
||||
|
||||
describe('IsIncludedPipe', () => {
|
||||
|
||||
let pipe: IsIncludedPipe<any>;
|
||||
const array = [1, 2, 'test', [null], {}];
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new IsIncludedPipe();
|
||||
});
|
||||
|
||||
it('should return true if the string is contained in an array', () => {
|
||||
expect(pipe.transform('test', array)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if the string is not contained in an array', () => {
|
||||
expect(pipe.transform('test 1', array)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true if the number is in the array', () => {
|
||||
expect(pipe.transform(2, array)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if the number is not contained in an array', () => {
|
||||
expect(pipe.transform(50, array)).toBeFalsy();
|
||||
});
|
||||
});
|
27
lib/content-services/src/lib/pipes/is-included.pipe.ts
Normal file
27
lib/content-services/src/lib/pipes/is-included.pipe.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'adfIsIncluded'
|
||||
})
|
||||
export class IsIncludedPipe<T> implements PipeTransform {
|
||||
transform(value: T, array: T[]): boolean {
|
||||
return array.includes(value);
|
||||
}
|
||||
}
|
@@ -16,4 +16,5 @@
|
||||
*/
|
||||
|
||||
export * from './node-name-tooltip.pipe';
|
||||
export * from './is-included.pipe';
|
||||
export * from './content-pipe.module';
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<mat-form-field class="adf-chip-list" appearance="outline">
|
||||
<mat-chip-list #chipList [attr.aria-label]="'SEARCH.FILTER.ARIA-LABEL.OPTIONS-SELECTION' | translate">
|
||||
<mat-chip
|
||||
class="adf-option-chips"
|
||||
*ngFor="let option of selectedOptions"
|
||||
(removed)="remove(option)">
|
||||
<span>{{option}}</span>
|
||||
<button matChipRemove class="adf-option-chips-delete-button" [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option">
|
||||
<mat-icon class="adf-option-chips-delete-icon">close</mat-icon>
|
||||
</button>
|
||||
</mat-chip>
|
||||
<input
|
||||
placeholder="{{ 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | translate }}"
|
||||
#optionInput
|
||||
[formControl]="formCtrl"
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[attr.aria-label]="'SEARCH.FILTER.ACTIONS.ADD_OPTION' | translate"
|
||||
(matChipInputTokenEnd)="add($event)">
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
|
||||
<mat-option [disabled]="option | adfIsIncluded: selectedOptions" *ngFor="let option of filteredOptions$ | async"
|
||||
[ngClass]="(option | adfIsIncluded: selectedOptions) && 'adf-autocomplete-added-option'">
|
||||
{{option}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
@@ -0,0 +1,46 @@
|
||||
adf-search-chip-autocomplete-input {
|
||||
.adf-chip-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-form-field-appearance-outline .mat-form-field-outline {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.mat-form-field-infix {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mat-chip.adf-option-chips {
|
||||
border: 1px solid var(--theme-text-color);
|
||||
border-radius: 10px;
|
||||
background-color: var(--theme-primary-color-default-contrast);
|
||||
height: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mat-chip-remove.adf-option-chips-delete-button {
|
||||
font-size: 13px;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
|
||||
.adf-option-chips-delete-icon.mat-icon {
|
||||
font-size: 13px;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-chip-list-wrapper {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.mat-form-field-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-option.adf-autocomplete-added-option {
|
||||
background: var(--adf-theme-mat-grey-color-a200);
|
||||
color: var(--adf-theme-primary-300);
|
||||
}
|
@@ -0,0 +1,177 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatChip, MatChipRemove } from '@angular/material/chips';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { SearchChipAutocompleteInputComponent } from './search-chip-autocomplete-input.component';
|
||||
|
||||
describe('SearchChipAutocompleteInputComponent', () => {
|
||||
let component: SearchChipAutocompleteInputComponent;
|
||||
let fixture: ComponentFixture<SearchChipAutocompleteInputComponent>;
|
||||
const onResetSubject = new Subject<void>();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SearchChipAutocompleteInputComponent],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.onReset$ = onResetSubject.asObservable();
|
||||
fixture.detectChanges();
|
||||
component.autocompleteOptions = ['option1', 'option2'];
|
||||
});
|
||||
|
||||
function enterNewInputValue(value: string) {
|
||||
const inputElement = fixture.debugElement.query(By.css('input'));
|
||||
inputElement.nativeElement.dispatchEvent(new Event('focusin'));
|
||||
inputElement.nativeElement.value = value;
|
||||
inputElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
function addNewOption(value: string) {
|
||||
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
inputElement.value = value;
|
||||
fixture.detectChanges();
|
||||
inputElement.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13}));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
function getChipList(): MatChip[] {
|
||||
return fixture.debugElement.queryAll(By.css('mat-chip')).map((chip) => chip.nativeElement);
|
||||
}
|
||||
|
||||
function getChipValue(index: number): string {
|
||||
return fixture.debugElement.queryAll(By.css('mat-chip span')).map((chip) => chip.nativeElement)[index].innerText;
|
||||
}
|
||||
|
||||
it('should add new option only if value is predefined when allowOnlyPredefinedValues = true', () => {
|
||||
addNewOption('test');
|
||||
addNewOption('option1');
|
||||
expect(getChipList().length).toBe(1);
|
||||
expect(getChipValue(0)).toBe('option1');
|
||||
});
|
||||
|
||||
it('should add new option even if value is not predefined when allowOnlyPredefinedValues = false', () => {
|
||||
component.allowOnlyPredefinedValues = false;
|
||||
addNewOption('test');
|
||||
addNewOption('option1');
|
||||
expect(getChipList().length).toBe(2);
|
||||
expect(getChipValue(0)).toBe('test');
|
||||
});
|
||||
|
||||
it('should add new option upon clicking on option from autocomplete', async () => {
|
||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||
enterNewInputValue('op');
|
||||
await fixture.whenStable();
|
||||
|
||||
const matOptions = document.querySelectorAll('mat-option');
|
||||
expect(matOptions.length).toBe(2);
|
||||
|
||||
const optionToClick = matOptions[0] as HTMLElement;
|
||||
optionToClick.click();
|
||||
|
||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
|
||||
expect(component.selectedOptions).toEqual(['option1']);
|
||||
expect(getChipList().length).toBe(1);
|
||||
});
|
||||
|
||||
it('should apply class to already selected options', async () => {
|
||||
addNewOption('option1');
|
||||
enterNewInputValue('op');
|
||||
|
||||
const addedOptions = fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option'));
|
||||
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(addedOptions[0]).toBeTruthy();
|
||||
expect(addedOptions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should limit autocomplete list to 15 values max', () => {
|
||||
component.autocompleteOptions = ['a1','a2','a3','a4','a5','a6','a7','a8','a9','a10','a11','a12','a13','a14','a15','a16'];
|
||||
enterNewInputValue('a');
|
||||
|
||||
const matOptions = document.querySelectorAll('mat-option');
|
||||
expect(matOptions.length).toBe(15);
|
||||
});
|
||||
|
||||
it('should not add a value if same value has already been added', () => {
|
||||
addNewOption('option1');
|
||||
addNewOption('option1');
|
||||
expect(getChipList().length).toBe(1);
|
||||
});
|
||||
|
||||
it('should show autocomplete list if similar predefined values exists', () => {
|
||||
enterNewInputValue('op');
|
||||
const matOptions = document.querySelectorAll('mat-option');
|
||||
expect(matOptions.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should not show autocomplete list if there are no similar predefined values', () => {
|
||||
enterNewInputValue('test');
|
||||
const matOptions = document.querySelectorAll('mat-option');
|
||||
expect(matOptions.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should emit new value when selected options changed', () => {
|
||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||
addNewOption('option1');
|
||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
|
||||
expect(getChipList().length).toBe(1);
|
||||
expect(getChipValue(0)).toBe('option1');
|
||||
});
|
||||
|
||||
it('should clear the input after a new value is added', () => {
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
addNewOption('option1');
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('should reset all options when onReset$ event is emitted', () => {
|
||||
addNewOption('option1');
|
||||
addNewOption('option2');
|
||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||
onResetSubject.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([]);
|
||||
expect(getChipList()).toEqual([]);
|
||||
expect(component.selectedOptions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove option upon clicking remove button', () => {
|
||||
addNewOption('option1');
|
||||
addNewOption('option2');
|
||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||
|
||||
fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option2']);
|
||||
expect(getChipList().length).toEqual(1);
|
||||
});
|
||||
});
|
@@ -0,0 +1,120 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, ElementRef, ViewChild, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ENTER } from '@angular/cdk/keycodes';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { map, startWith, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-chip-autocomplete-input',
|
||||
templateUrl: './search-chip-autocomplete-input.component.html',
|
||||
styleUrls: ['./search-chip-autocomplete-input.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('optionInput')
|
||||
optionInput: ElementRef<HTMLInputElement>;
|
||||
|
||||
@Input()
|
||||
autocompleteOptions: string[] = [];
|
||||
|
||||
@Input()
|
||||
onReset$: Observable<void>;
|
||||
|
||||
@Input()
|
||||
allowOnlyPredefinedValues = true;
|
||||
|
||||
@Output()
|
||||
optionsChanged: EventEmitter<string[]> = new EventEmitter();
|
||||
|
||||
readonly separatorKeysCodes = [ENTER] as const;
|
||||
formCtrl = new FormControl('');
|
||||
filteredOptions$: Observable<string[]>;
|
||||
selectedOptions: string[] = [];
|
||||
private onDestroy$ = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
this.filteredOptions$ = this.formCtrl.valueChanges.pipe(
|
||||
startWith(null),
|
||||
map((value: string | null) => (value ? this.filter(value) : []))
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next();
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
add(event: MatChipInputEvent) {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
if (value && this.isExists(value) && !this.isAdded(value)) {
|
||||
this.selectedOptions.push(value);
|
||||
this.optionsChanged.emit(this.selectedOptions);
|
||||
event.chipInput.clear();
|
||||
this.formCtrl.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
remove(value: string) {
|
||||
const index = this.selectedOptions.indexOf(value);
|
||||
|
||||
if (index >= 0) {
|
||||
this.selectedOptions.splice(index, 1);
|
||||
this.optionsChanged.emit(this.selectedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
selected(event: MatAutocompleteSelectedEvent) {
|
||||
if (!this.isAdded(event.option.viewValue)) {
|
||||
this.selectedOptions.push(event.option.viewValue);
|
||||
this.optionInput.nativeElement.value = '';
|
||||
this.formCtrl.setValue(null);
|
||||
this.optionsChanged.emit(this.selectedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private filter(value: string): string[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
return this.autocompleteOptions.filter(option => option.toLowerCase().includes(filterValue)).slice(0, 15);
|
||||
}
|
||||
|
||||
private isAdded(value: string): boolean {
|
||||
return this.selectedOptions.includes(value);
|
||||
}
|
||||
|
||||
private isExists(value: string): boolean {
|
||||
return this.allowOnlyPredefinedValues
|
||||
? this.autocompleteOptions.map(option => option.toLowerCase()).includes(value.toLowerCase())
|
||||
: true;
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.selectedOptions = [];
|
||||
this.optionsChanged.emit(this.selectedOptions);
|
||||
this.formCtrl.setValue(null);
|
||||
this.optionInput.nativeElement.value = '';
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<adf-search-chip-autocomplete-input
|
||||
[autocompleteOptions]="autocompleteOptions"
|
||||
[onReset$]="reset$"
|
||||
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
|
||||
(optionsChanged)="onOptionsChange($event)">
|
||||
</adf-search-chip-autocomplete-input>
|
||||
|
||||
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" data-automation-id="adf-search-chip-autocomplete-btn-clear" (click)="reset()">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" data-automation-id="adf-search-chip-autocomplete-btn-apply" (click)="submitValues()">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.APPLY' | translate }}
|
||||
</button>
|
||||
</div>
|
@@ -0,0 +1,122 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
|
||||
import { TagService } from '@alfresco/adf-content-services';
|
||||
import { EMPTY, of } from 'rxjs';
|
||||
|
||||
describe('SearchFilterAutocompleteChipsComponent', () => {
|
||||
let component: SearchFilterAutocompleteChipsComponent;
|
||||
let fixture: ComponentFixture<SearchFilterAutocompleteChipsComponent>;
|
||||
let tagService: TagService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SearchFilterAutocompleteChipsComponent],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [{
|
||||
provide: TagService,
|
||||
useValue: { getAllTheTags: () => EMPTY }
|
||||
}]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SearchFilterAutocompleteChipsComponent);
|
||||
component = fixture.componentInstance;
|
||||
tagService = TestBed.inject(TagService);
|
||||
component.id = 'test-id';
|
||||
component.context = {
|
||||
queryFragments: {},
|
||||
update: () => EMPTY
|
||||
} as any;
|
||||
component.settings = {
|
||||
field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false,
|
||||
options: ['option1', 'option2']
|
||||
};
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
function addNewOption(value: string) {
|
||||
const inputElement = fixture.debugElement.query(By.css('adf-search-chip-autocomplete-input input')).nativeElement;
|
||||
inputElement.value = value;
|
||||
inputElement.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13}));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
it('should set autocomplete options on init', () => {
|
||||
component.settings.options = ['test 1', 'test 2'];
|
||||
component.ngOnInit();
|
||||
expect(component.autocompleteOptions).toEqual(['test 1', 'test 2']);
|
||||
});
|
||||
|
||||
it('should load tags if field = TAG', () => {
|
||||
const tagPagingMock = {
|
||||
list: {
|
||||
pagination: {},
|
||||
entries: [{entry: {tag: 'tag1', id: 'id1'}}, {entry: {tag: 'tag2', id: 'id2'}}]
|
||||
}
|
||||
};
|
||||
|
||||
component.settings.field = 'TAG';
|
||||
spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock));
|
||||
component.ngOnInit();
|
||||
expect(component.autocompleteOptions).toEqual(['tag1', 'tag2']);
|
||||
});
|
||||
|
||||
it('should update display value when options changes', () => {
|
||||
const newOption = 'option1';
|
||||
spyOn(component, 'onOptionsChange').and.callThrough();
|
||||
spyOn(component.displayValue$, 'next');
|
||||
addNewOption(newOption);
|
||||
|
||||
expect(component.onOptionsChange).toHaveBeenCalled();
|
||||
expect(component.displayValue$.next).toHaveBeenCalledOnceWith(newOption);
|
||||
});
|
||||
|
||||
it('should reset value and display value when reset button is clicked', () => {
|
||||
component.setValue(['option1', 'option2']);
|
||||
fixture.detectChanges();
|
||||
expect(component.selectedOptions).toEqual(['option1', 'option2']);
|
||||
spyOn(component.context, 'update');
|
||||
spyOn(component.displayValue$, 'next');
|
||||
const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement;
|
||||
clearBtn.click();
|
||||
|
||||
expect(component.context.queryFragments[component.id]).toBe('');
|
||||
expect(component.context.update).toHaveBeenCalled();
|
||||
expect(component.selectedOptions).toEqual( [] );
|
||||
expect(component.displayValue$.next).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should correctly compose the search query', () => {
|
||||
spyOn(component.context, 'update');
|
||||
addNewOption('option2');
|
||||
addNewOption('option1');
|
||||
const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement;
|
||||
applyBtn.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.context.update).toHaveBeenCalled();
|
||||
expect(component.context.queryFragments[component.id]).toBe('test: "option2" OR test: "option1"');
|
||||
});
|
||||
});
|
@@ -0,0 +1,108 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { TagService } from '../../../tag/services/tag.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-filter-autocomplete-chips',
|
||||
templateUrl: './search-filter-autocomplete-chips.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnInit {
|
||||
id: string;
|
||||
settings?: SearchWidgetSettings;
|
||||
context?: SearchQueryBuilderService;
|
||||
options: SearchFilterList<string[]>;
|
||||
startValue: string[] = null;
|
||||
displayValue$ = new Subject<string>();
|
||||
|
||||
private resetSubject$ = new Subject<void>();
|
||||
reset$: Observable<void> = this.resetSubject$.asObservable();
|
||||
autocompleteOptions: string[] = [];
|
||||
selectedOptions: string[] = [];
|
||||
enableChangeUpdate: boolean;
|
||||
|
||||
constructor( private tagService: TagService ) {
|
||||
this.options = new SearchFilterList<string[]>();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.settings) {
|
||||
this.setOptions();
|
||||
if (this.startValue) {
|
||||
this.setValue(this.startValue);
|
||||
}
|
||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectedOptions = [];
|
||||
this.resetSubject$.next();
|
||||
this.updateQuery();
|
||||
}
|
||||
|
||||
submitValues() {
|
||||
this.updateQuery();
|
||||
}
|
||||
|
||||
hasValidValue(): boolean {
|
||||
return !!this.selectedOptions;
|
||||
}
|
||||
|
||||
getCurrentValue(): string[]{
|
||||
return this.selectedOptions;
|
||||
}
|
||||
|
||||
onOptionsChange(selectedOptions: string[]) {
|
||||
this.selectedOptions = selectedOptions;
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateQuery();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: string[]) {
|
||||
this.selectedOptions = value;
|
||||
this.displayValue$.next(this.selectedOptions.join(', '));
|
||||
this.submitValues();
|
||||
}
|
||||
|
||||
private updateQuery() {
|
||||
this.displayValue$.next(this.selectedOptions.join(', '));
|
||||
if (this.context && this.settings && this.settings.field) {
|
||||
this.context.queryFragments[this.id] = this.selectedOptions.map(val => `${this.settings.field}: "${val}"`).join(' OR ');
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
private setOptions() {
|
||||
if (this.settings.field === 'TAG') {
|
||||
this.tagService.getAllTheTags().subscribe(res => {
|
||||
this.autocompleteOptions = res.list.entries.map(tag => tag.entry.tag);
|
||||
});
|
||||
} else {
|
||||
this.autocompleteOptions = this.settings.options;
|
||||
}
|
||||
}
|
||||
}
|
@@ -25,6 +25,8 @@ export interface SearchWidgetSettings {
|
||||
unit?: string;
|
||||
/* describes query format */
|
||||
format?: string;
|
||||
/* allow the user to search only within predefined options */
|
||||
allowOnlyPredefinedValues?: boolean;
|
||||
|
||||
[indexer: string]: any;
|
||||
}
|
||||
|
@@ -63,5 +63,7 @@ export * from './components/search-facet-field/search-facet-field.component';
|
||||
export * from './components/search-chip-input/search-chip-input.component';
|
||||
export * from './components/search-logical-filter/search-logical-filter.component';
|
||||
export * from './components/reset-search.directive';
|
||||
export * from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component';
|
||||
export * from './components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component';
|
||||
|
||||
export * from './search.module';
|
||||
|
@@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { ContentPipeModule } from '../pipes/content-pipe.module';
|
||||
|
||||
import { CoreModule, SearchTextModule } from '@alfresco/adf-core';
|
||||
|
||||
@@ -29,6 +30,8 @@ import { SearchWidgetContainerComponent } from './components/search-widget-conta
|
||||
import { SearchFilterComponent } from './components/search-filter/search-filter.component';
|
||||
import { SearchChipListComponent } from './components/search-chip-list/search-chip-list.component';
|
||||
import { SearchTextComponent } from './components/search-text/search-text.component';
|
||||
import { SearchChipAutocompleteInputComponent } from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component';
|
||||
import { SearchFilterAutocompleteChipsComponent } from './components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component';
|
||||
import { SearchRadioComponent } from './components/search-radio/search-radio.component';
|
||||
import { SearchSliderComponent } from './components/search-slider/search-slider.component';
|
||||
import { SearchNumberRangeComponent } from './components/search-number-range/search-number-range.component';
|
||||
@@ -53,6 +56,7 @@ import { ResetSearchDirective } from './components/reset-search.directive';
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ContentPipeModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MaterialModule,
|
||||
@@ -67,6 +71,8 @@ import { ResetSearchDirective } from './components/reset-search.directive';
|
||||
SearchChipListComponent,
|
||||
SearchWidgetContainerComponent,
|
||||
SearchTextComponent,
|
||||
SearchChipAutocompleteInputComponent,
|
||||
SearchFilterAutocompleteChipsComponent,
|
||||
SearchRadioComponent,
|
||||
SearchSliderComponent,
|
||||
SearchNumberRangeComponent,
|
||||
@@ -94,6 +100,8 @@ import { ResetSearchDirective } from './components/reset-search.directive';
|
||||
SearchChipListComponent,
|
||||
SearchWidgetContainerComponent,
|
||||
SearchTextComponent,
|
||||
SearchChipAutocompleteInputComponent,
|
||||
SearchFilterAutocompleteChipsComponent,
|
||||
SearchRadioComponent,
|
||||
SearchSliderComponent,
|
||||
SearchNumberRangeComponent,
|
||||
|
@@ -24,6 +24,7 @@ import { SearchCheckListComponent } from '../components/search-check-list/search
|
||||
import { SearchDateRangeComponent } from '../components/search-date-range/search-date-range.component';
|
||||
import { SearchDatetimeRangeComponent } from '../components/search-datetime-range/search-datetime-range.component';
|
||||
import { SearchLogicalFilterComponent } from '../components/search-logical-filter/search-logical-filter.component';
|
||||
import { SearchFilterAutocompleteChipsComponent } from '../components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -41,7 +42,8 @@ export class SearchFilterService {
|
||||
'check-list': SearchCheckListComponent,
|
||||
'date-range': SearchDateRangeComponent,
|
||||
'datetime-range': SearchDatetimeRangeComponent,
|
||||
'logical-filter': SearchLogicalFilterComponent
|
||||
'logical-filter': SearchLogicalFilterComponent,
|
||||
'autocomplete-chips': SearchFilterAutocompleteChipsComponent
|
||||
};
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user