[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:
Mykyta Maliarchuk 2023-06-14 12:29:43 +02:00 committed by GitHub
parent 5fafb0ea6f
commit 61d5aa965b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 866 additions and 6 deletions

View File

@ -287,10 +287,12 @@ for more information about installing and using the source code.
| [Rating component](content-services/components/rating.component.md) | Allows a user to add and remove rating to an item. | [Source](../lib/content-services/src/lib/social/rating.component.ts) |
| [Search check list component](content-services/components/search-check-list.component.md) | Implements a checklist widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts) |
| [Search Chip Input Component](content-services/components/search-chip-input.component.md) | Displays input for providing phrases display as "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts) |
| [Search Chip Autocomplete Input component](content-services/components/search-chip-autocomplete-input.component.md) | Displays an input with autocomplete options. | [Source](../lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts) |
| [Search Chip List Component](content-services/components/search-chip-list.component.md) | Displays search criteria as a set of "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-list/search-chip-list.component.ts) |
| [Search control component](content-services/components/search-control.component.md) | Displays a input text that shows find-as-you-type suggestions. | [Source](../lib/content-services/src/lib/search/components/search-control.component.ts) |
| [Search date range component](content-services/components/search-date-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-date-range/search-date-range.component.ts) |
| [Search datetime range component](content-services/components/search-datetime-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-datetime-range/search-datetime-range.component.ts) |
| [Search Filter Autocomplete Chips component](content-services/components/search-filter-autocomplete-chips.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts) |
| [Search Filter Chips component](content-services/components/search-filter-chips.component.md) | Represents a chip based container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts) |
| [Search Filter component](content-services/components/search-filter.component.md) | Represents a main container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts) |
| [Search Form component](content-services/components/search-form.component.md) | Search Form screenshot | [Source](../lib/content-services/src/lib/search/components/search-form/search-form.component.ts) |

View File

@ -0,0 +1,52 @@
---
Title: Search Chip Autocomplete Input component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-13
---
# [Search Chip Autocomplete Input component](../../../lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts "Defined in search-chip-autocomplete-input.component.ts")
Represents an input with autocomplete options.
![Search Chip Autocomplete Input](../../docassets/images/search-chip-autocomplete-input.png)
## Basic usage
```html
<adf-search-chip-autocomplete-input
[autocompleteOptions]="allOptions"
[onReset$]="onResetObservable$"
[allowOnlyPredefinedValues]="allowOnlyPredefinedValues"
(optionsChanged)="onOptionsChange($event)">
</adf-search-chip-autocomplete-input>
```
### Properties
| Name | Type | Default value | Description |
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------|
| autocompleteOptions | `string[]` | [] | Options for autocomplete |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
### Events
| Name | Type | Description |
| ---- | ---- |-----------------------------------------------|
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when the selected options are changed |
## See also
- [Search Configuration Guide](../../user-guide/search-configuration-guide.md)
- [Search Query Builder service](../services/search-query-builder.service.md)
- [Search Widget Interface](../interfaces/search-widget.interface.md)
- [Search Filter Autocomplete Chips component](search-filter-autocomplete-chips.component.md)
- [Search Logical Filter component](search-logical-filter.component.md)
- [Search check list component](search-check-list.component.md)
- [Search date range component](search-date-range.component.md)
- [Search number range component](search-number-range.component.md)
- [Search radio component](search-radio.component.md)
- [Search slider component](search-slider.component.md)
- [Search text component](search-text.component.md)
- [Search Chip Input component](search-chip-input.component.md)

View File

@ -0,0 +1,67 @@
---
Title: Search Filter Autocomplete Chips component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-13
---
# [Search Filter Autocomplete Chips component](../../../lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts "Defined in search-filter-autocomplete-chips.component.ts")
Implements a [search widget](../../../lib/content-services/src/lib/search/models/search-widget.interface.ts) consists of 1 input with autocomplete options representing conditions to form search query.
![Search Filter Autocomplete Chips](../../docassets/images/search-filter-autocomplete-chips.png)
## Basic usage
```json
{
"search": {
"categories": [
{
"id": "location",
"name": "Location",
"enabled": true,
"component": {
"selector": "autocomplete-chips",
"settings": {
"allowUpdateOnChange": false,
"hideDefaultAction": true,
"allowOnlyPredefinedValues": false,
"field": "SITE",
"options": [ "Option 1", "Option 2" ]
}
}
}
]
}
}
```
### Settings
| Name | Type | Description |
| ---- |----------|--------------------------------------------------------------------------------------------------------------------|
| field | `string` | Field to apply the query to. Required value |
| options | `string[]` | Predefined options for autocomplete |
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
## Details
This component allows the user to choose filter options for the search query.
See the [Search Chip Autocomplete Input component](search-chip-autocomplete-input.component.md) for more details.
## See also
- [Search Configuration Guide](../../user-guide/search-configuration-guide.md)
- [Search Query Builder service](../services/search-query-builder.service.md)
- [Search Widget Interface](../interfaces/search-widget.interface.md)
- [Search Chip Autocomplete Input component](search-chip-autocomplete-input.component.md)
- [Search Chip Input component](search-chip-input.component.md)
- [Search check list component](search-check-list.component.md)
- [Search date range component](search-date-range.component.md)
- [Search number range component](search-number-range.component.md)
- [Search radio component](search-radio.component.md)
- [Search slider component](search-slider.component.md)
- [Search text component](search-text.component.md)
- [Search Logical Filter component](search-logical-filter.component.md)

View File

@ -0,0 +1,28 @@
---
Title: Is Included pipe
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-13
---
# [Is Included pipe](../../../lib/content-services/src/lib/pipes/is-included.pipe.ts "Defined in is-included.pipe.ts")
Checks if the provided value is contained in the provided array.
## Basic Usage
<!-- {% raw %} -->
```HTML
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues"</mat-option>
```
<!-- {% endraw %} -->
## Details
The pipe takes the provided value and checks if that value is included in the provided array and returns the appropriate boolean value.
## See also
- [File upload error pipe](./file-upload-error.pipe.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -49,6 +49,9 @@ backend services have been tested with each released version of ADF.
- [Use none component view encapsulation](eslint-angular/rules/use-none-component-view-encapsulation.md)
- [Search Chip Input component](content-services/components/search-chip-input.component.md)
- [Search Logical Filter component](content-services/components/search-logical-filter.component.md)
- [Search Chip Autocomplete Input component](content-services/components/search-chip-autocomplete-input.component.md)
- [Search Filter Autocomplete Chips component](content-services/components/search-filter-autocomplete-chips.component.md)
- [Is Included pipe](content-services/pipes/is-included.pipe.md)
<!--v610 end-->

View File

@ -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"
},

View File

@ -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 {

View 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();
});
});

View 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);
}
}

View File

@ -16,4 +16,5 @@
*/
export * from './node-name-tooltip.pipe';
export * from './is-included.pipe';
export * from './content-pipe.module';

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
});
});

View File

@ -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 = '';
}
}

View File

@ -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>

View File

@ -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"');
});
});

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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,

View File

@ -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
};
}