[ACS-5266] Advanced Search - New component for Category facet (#8764)

* [ACS-5266] new component for category facet

* [ACS-5266] fixed tests & docs

* [ACS-5266] some fixes

* [ACS-5266] linting

* [ACS-5266] some improvements

* [ACS-5266] reduced observable from child component

* [ACS-5266] fixed docs

* [ACS-5266] rebase & improvements

* [ACS-5266] typo
This commit is contained in:
Mykyta Maliarchuk
2023-07-21 09:27:31 +02:00
committed by GitHub
parent 1ebac21251
commit 2a4507d529
18 changed files with 308 additions and 126 deletions

View File

@@ -15,30 +15,32 @@ Represents an input with autocomplete options.
```html ```html
<adf-search-chip-autocomplete-input <adf-search-chip-autocomplete-input
[autocompleteOptions]="allOptions" [autocompleteOptions]="autocompleteOptions"
[onReset$]="onResetObservable$" [onReset$]="onResetObservable$"
[allowOnlyPredefinedValues]="allowOnlyPredefinedValues" [allowOnlyPredefinedValues]="allowOnlyPredefinedValues"
(inputChanged)="onInputChange($event)"
(optionsChanged)="onOptionsChange($event)"> (optionsChanged)="onOptionsChange($event)">
</adf-search-chip-autocomplete-input> </adf-search-chip-autocomplete-input>
``` ```
### Properties ### Properties
| Name | Type | Default value | Description | | Name | Type | Default value | Description |
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------| |---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------------------------------------------------------|
| autocompleteOptions | `string[]` | [] | Options for autocomplete | | autocompleteOptions | `AutocompleteOption[]` | [] | 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 | | 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 | | allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. | | placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: string, option2: string) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. | | compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. | | formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
| filter | (options: string[], value: string) => string[] | | Function which is used to filter out possibile options from hint. By default it checks if option includes typed value and is case insensitive. | | filter | (options: AutocompleteOption[], value: string) => AutocompleteOption[] | | Function which is used to filter out possible options from hint. By default it checks if option includes typed value and is case insensitive. |
### Events ### Events
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- |-----------------------------------------------| | ---- | ---- |-----------------------------------------------|
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when the selected options are changed | | inputChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the input changes |
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<AutocompleteOption[]>` | Emitted when the selected options are changed |
## See also ## See also

View File

@@ -28,7 +28,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
"hideDefaultAction": true, "hideDefaultAction": true,
"allowOnlyPredefinedValues": false, "allowOnlyPredefinedValues": false,
"field": "SITE", "field": "SITE",
"options": [ "Option 1", "Option 2" ] "autocompleteOptions": [ {"value": "Option 1"}, {"value": "Option 2"} ]
} }
} }
} }
@@ -42,7 +42,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
| Name | Type | Description | | Name | Type | Description |
| ---- |----------|--------------------------------------------------------------------------------------------------------------------| | ---- |----------|--------------------------------------------------------------------------------------------------------------------|
| field | `string` | Field to apply the query to. Required value | | field | `string` | Field to apply the query to. Required value |
| options | `string[]` | Predefined options for autocomplete | | autocompleteOptions | `AutocompleteOption[]` | Predefined options for autocomplete |
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined | | 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 | | 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 | | hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |

View File

@@ -14,7 +14,7 @@ Checks if the provided value is contained in the provided array.
<!-- {% raw %} --> <!-- {% raw %} -->
```HTML ```HTML
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues"</mat-option> <mat-option [disabled]="value | adfIsIncluded: arrayOfValues : comparator"></mat-option>
``` ```
<!-- {% endraw %} --> <!-- {% endraw %} -->

View File

@@ -284,6 +284,7 @@
"SUMMARY": "{{numResults}} result found for {{searchTerm}}", "SUMMARY": "{{numResults}} result found for {{searchTerm}}",
"NONE": "No results found for {{searchTerm}}", "NONE": "No results found for {{searchTerm}}",
"ERROR": "We hit a problem during the search - try again.", "ERROR": "We hit a problem during the search - try again.",
"WILL_CONTAIN": "Results will contain '{{searchTerm}}'",
"COLUMNS": { "COLUMNS": {
"NAME": "Display name", "NAME": "Display name",
"MODIFIED_BY": "Modified by", "MODIFIED_BY": "Modified by",

View File

@@ -19,6 +19,7 @@ import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatOptionModule, MatRippleModule } from '@angular/material/core'; import { MatOptionModule, MatRippleModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -40,6 +41,7 @@ import { MatBadgeModule } from '@angular/material/badge';
@NgModule({ @NgModule({
imports: [ imports: [
MatButtonModule, MatButtonModule,
MatAutocompleteModule,
MatChipsModule, MatChipsModule,
MatDialogModule, MatDialogModule,
MatIconModule, MatIconModule,
@@ -63,6 +65,7 @@ import { MatBadgeModule } from '@angular/material/badge';
], ],
exports: [ exports: [
MatButtonModule, MatButtonModule,
MatAutocompleteModule,
MatChipsModule, MatChipsModule,
MatDialogModule, MatDialogModule,
MatIconModule, MatIconModule,

View File

@@ -41,4 +41,11 @@ describe('IsIncludedPipe', () => {
it('should return false if the number is not contained in an array', () => { it('should return false if the number is not contained in an array', () => {
expect(pipe.transform(50, array)).toBeFalsy(); expect(pipe.transform(50, array)).toBeFalsy();
}); });
it('should use provided comparator to check if value contains in the provided array', () => {
const arrayOfObjects = [{id: 'id-1', value: 'value-1'}, {id: 'id-2', value: 'value-2'}];
const filterFunction = (extension1, extension2) => extension1.value === extension2.value;
expect(pipe.transform({id: 'id-1', value: 'value-1'}, arrayOfObjects, filterFunction)).toBeTruthy();
expect(pipe.transform({id: 'id-1', value: 'value-3'}, arrayOfObjects, filterFunction)).toBeFalsy();
});
}); });

View File

@@ -4,8 +4,14 @@
class="adf-option-chips" class="adf-option-chips"
*ngFor="let option of selectedOptions" *ngFor="let option of selectedOptions"
(removed)="remove(option)"> (removed)="remove(option)">
<span>{{option}}</span> <span [matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath}"
<button matChipRemove class="adf-option-chips-delete-button" [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option"> [matTooltipDisabled]="!option.fullPath" [matTooltipShowDelay]="tooltipShowDelay">
{{ option.value }}
</span>
<button matChipRemove class="adf-option-chips-delete-button" [matTooltipDisabled]="!option.fullPath"
[matTooltip]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' \'' + option.fullPath + '\''"
[matTooltipShowDelay]="tooltipShowDelay"
[attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value">
<mat-icon class="adf-option-chips-delete-icon">close</mat-icon> <mat-icon class="adf-option-chips-delete-icon">close</mat-icon>
</button> </button>
</mat-chip> </mat-chip>
@@ -24,9 +30,15 @@
</mat-chip-list> </mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete" <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false"> (optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
<mat-option [disabled]="option | adfIsIncluded: selectedOptions : compareOption" *ngFor="let option of filteredOptions$ | async" <ng-container *ngIf="optionInput.value.length > 0">
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'"> <mat-option
{{option}} [disabled]="option | adfIsIncluded: selectedOptions : compareOption"
</mat-option> *ngFor="let option of filteredOptions" [value]="option" [matTooltipShowDelay]="tooltipShowDelay"
[matTooltipDisabled]="!option.fullPath" matTooltipPosition="right"
[matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath || option.value}"
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
{{ option.fullPath || option.value }}
</mat-option>
</ng-container>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>

View File

@@ -40,6 +40,10 @@ adf-search-chip-autocomplete-input {
} }
} }
.mat-tooltip-hide {
display: none;
}
.mat-option.adf-autocomplete-added-option { .mat-option.adf-autocomplete-added-option {
background: var(--adf-theme-mat-grey-color-a200); background: var(--adf-theme-mat-grey-color-a200);
color: var(--adf-theme-primary-300); color: var(--adf-theme-primary-300);

View File

@@ -41,8 +41,8 @@ describe('SearchChipAutocompleteInputComponent', () => {
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent); fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.onReset$ = onResetSubject.asObservable(); component.onReset$ = onResetSubject.asObservable();
component.autocompleteOptions = [{value: 'option1'}, {value: 'option2'}];
fixture.detectChanges(); fixture.detectChanges();
component.autocompleteOptions = ['option1', 'option2'];
}); });
function getInput(): HTMLInputElement { function getInput(): HTMLInputElement {
@@ -110,6 +110,7 @@ describe('SearchChipAutocompleteInputComponent', () => {
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
enterNewInputValue('op'); enterNewInputValue('op');
await fixture.whenStable(); await fixture.whenStable();
fixture.detectChanges();
const matOptions = getOptionElements(); const matOptions = getOptionElements();
expect(matOptions.length).toBe(2); expect(matOptions.length).toBe(2);
@@ -117,8 +118,8 @@ describe('SearchChipAutocompleteInputComponent', () => {
const optionToClick = matOptions[0].nativeElement as HTMLElement; const optionToClick = matOptions[0].nativeElement as HTMLElement;
optionToClick.click(); optionToClick.click();
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']); expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
expect(component.selectedOptions).toEqual(['option1']); expect(component.selectedOptions).toEqual([{value: 'option1'}]);
expect(getChipList().length).toBe(1); expect(getChipList().length).toBe(1);
}); });
@@ -126,31 +127,35 @@ describe('SearchChipAutocompleteInputComponent', () => {
addNewOption('option1'); addNewOption('option1');
enterNewInputValue('op'); enterNewInputValue('op');
const addedOptions = getAddedOptionElements();
await fixture.whenStable(); await fixture.whenStable();
fixture.detectChanges();
const addedOptions = getAddedOptionElements();
expect(addedOptions[0]).toBeTruthy(); expect(addedOptions[0]).toBeTruthy();
expect(addedOptions.length).toBe(1); expect(addedOptions.length).toBe(1);
}); });
it('should apply class to already selected options based on custom compareOption function', async () => { it('should apply class to already selected options based on custom compareOption function', async () => {
component.allowOnlyPredefinedValues = false; component.allowOnlyPredefinedValues = false;
component.autocompleteOptions = ['.test1', 'test3', '.test2', 'test1.']; component.autocompleteOptions = [{value: '.test1'}, {value: 'test3'}, {value: '.test2.'}, {value: 'test1'}];
component.compareOption = (option1, option2) => option1.split('.')[1] === option2; component.compareOption = (option1, option2) => option1.value.split('.')[1] === option2.value;
fixture.detectChanges();
addNewOption('test1'); addNewOption('test1');
enterNewInputValue('t'); enterNewInputValue('t');
const addedOptions = getAddedOptionElements();
await fixture.whenStable(); await fixture.whenStable();
expect(addedOptions.length).toBe(1); fixture.detectChanges();
expect(getAddedOptionElements().length).toBe(1);
}); });
it('should limit autocomplete list to 15 values max', () => { it('should limit autocomplete list to 15 values max', async () => {
component.autocompleteOptions = ['a1','a2','a3','a4','a5','a6','a7','a8','a9','a10','a11','a12','a13','a14','a15','a16']; component.autocompleteOptions = Array.from({length: 16}, (_, i) => ({value: `a${i}`}));
enterNewInputValue('a'); enterNewInputValue('a');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(15); expect(getOptionElements().length).toBe(15);
}); });
@@ -160,27 +165,33 @@ describe('SearchChipAutocompleteInputComponent', () => {
expect(getChipList().length).toBe(1); expect(getChipList().length).toBe(1);
}); });
it('should show autocomplete list if similar predefined values exists', () => { it('should show autocomplete list if similar predefined values exists', async () => {
enterNewInputValue('op'); enterNewInputValue('op');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(2); expect(getOptionElements().length).toBe(2);
}); });
it('should show autocomplete list based on custom filtering', () => { it('should show autocomplete list based on custom filtering', async () => {
component.autocompleteOptions = ['.test1', 'test1', 'test1.', '.test2', '.test12']; component.autocompleteOptions = [{value: '.test1'}, {value: 'test1'}, {value: 'test1.'}, {value: '.test2'}, {value: '.test12'}];
component.filter = (options, value) => options.filter((option) => option.split('.')[1] === value); component.filter = (options, value) => options.filter((option) => option.value.split('.')[1] === value);
enterNewInputValue('test1'); enterNewInputValue('test1');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(1); expect(getOptionElements().length).toBe(1);
}); });
it('should not show autocomplete list if there are no similar predefined values', () => { it('should not show autocomplete list if there are no similar predefined values', async () => {
enterNewInputValue('test'); enterNewInputValue('test');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(0); expect(getOptionElements().length).toBe(0);
}); });
it('should emit new value when selected options changed', () => { it('should emit new value when selected options changed', () => {
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
addNewOption('option1'); addNewOption('option1');
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']); expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
expect(getChipList().length).toBe(1); expect(getChipList().length).toBe(1);
expect(getChipValue(0)).toBe('option1'); expect(getChipValue(0)).toBe('option1');
}); });
@@ -221,7 +232,23 @@ describe('SearchChipAutocompleteInputComponent', () => {
fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click(); fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option2']); expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option2'}]);
expect(getChipList().length).toEqual(1); expect(getChipList().length).toEqual(1);
}); });
it('should show full category path when fullPath provided', () => {
component.filteredOptions = [{id: 'test-id', value: 'test-value', fullPath: 'test-full-path'}];
enterNewInputValue('test-value');
const matOption = fixture.debugElement.query(By.css('.mat-option span')).nativeElement;
fixture.detectChanges();
expect(matOption.innerHTML).toEqual(' test-full-path ');
});
it('should emit input value when input changed', async () => {
const inputChangedSpy = spyOn(component.inputChanged, 'emit');
enterNewInputValue('test-value');
await fixture.whenStable();
expect(inputChangedSpy).toHaveBeenCalledOnceWith('test-value');
});
}); });

View File

@@ -15,13 +15,26 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, ViewEncapsulation, ElementRef, ViewChild, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; import {
Component,
ViewEncapsulation,
ElementRef,
ViewChild,
OnInit,
OnDestroy,
Input,
Output,
EventEmitter,
SimpleChanges,
OnChanges
} from '@angular/core';
import { ENTER } from '@angular/cdk/keycodes'; import { ENTER } from '@angular/cdk/keycodes';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { Observable, Subject } from 'rxjs'; import { EMPTY, Observable, Subject, timer } from 'rxjs';
import { map, startWith, takeUntil, tap } from 'rxjs/operators'; import { debounce, startWith, takeUntil, tap } from 'rxjs/operators';
import { AutocompleteOption } from '../../models/autocomplete-option.interface';
@Component({ @Component({
selector: 'adf-search-chip-autocomplete-input', selector: 'adf-search-chip-autocomplete-input',
@@ -29,12 +42,12 @@ import { map, startWith, takeUntil, tap } from 'rxjs/operators';
styleUrls: ['./search-chip-autocomplete-input.component.scss'], styleUrls: ['./search-chip-autocomplete-input.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy, OnChanges {
@ViewChild('optionInput') @ViewChild('optionInput')
optionInput: ElementRef<HTMLInputElement>; optionInput: ElementRef<HTMLInputElement>;
@Input() @Input()
autocompleteOptions: string[] = []; autocompleteOptions: AutocompleteOption[] = [];
@Input() @Input()
onReset$: Observable<void>; onReset$: Observable<void>;
@@ -46,24 +59,28 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION'; placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION';
@Input() @Input()
compareOption?: (option1: string, option2: string) => boolean; compareOption?: (option1: AutocompleteOption, option2: AutocompleteOption) => boolean;
@Input() @Input()
formatChipValue?: (option: string) => string; formatChipValue?: (option: string) => string;
@Input() @Input()
filter = (options: string[], value: string): string[] => { filter = (options: AutocompleteOption[], value: string): AutocompleteOption[] => {
const filterValue = value.toLowerCase(); const filterValue = value.toLowerCase();
return options.filter(option => option.toLowerCase().includes(filterValue)); return options.filter(option => option.value.toLowerCase().includes(filterValue)).slice(0, 15);
}; };
@Output() @Output()
optionsChanged: EventEmitter<string[]> = new EventEmitter(); optionsChanged = new EventEmitter<AutocompleteOption[]>();
@Output()
inputChanged = new EventEmitter<string>();
readonly separatorKeysCodes = [ENTER] as const; readonly separatorKeysCodes = [ENTER] as const;
formCtrl = new FormControl(''); formCtrl = new FormControl('');
filteredOptions$: Observable<string[]>; filteredOptions: AutocompleteOption[] = [];
selectedOptions: string[] = []; selectedOptions: AutocompleteOption[] = [];
tooltipShowDelay = 800;
private onDestroy$ = new Subject<void>(); private onDestroy$ = new Subject<void>();
private _activeAnyOption = false; private _activeAnyOption = false;
@@ -71,16 +88,25 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
this._activeAnyOption = active; this._activeAnyOption = active;
} }
constructor() { ngOnInit() {
this.filteredOptions$ = this.formCtrl.valueChanges.pipe( this.formCtrl.valueChanges
startWith(null), .pipe(
tap(() => this.activeAnyOption = false), startWith(''),
map((value: string | null) => (value ? this.filter(this.autocompleteOptions, value).slice(0, 15) : [])) tap(() => this.activeAnyOption = false),
); debounce((value: string) => (value ? timer(300) : EMPTY)),
takeUntil(this.onDestroy$)
)
.subscribe((value: string) => {
this.filteredOptions = value ? this.filter(this.autocompleteOptions, value) : [];
this.inputChanged.emit(value);
});
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset());
} }
ngOnInit() { ngOnChanges(changes: SimpleChanges) {
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset()); if (changes.autocompleteOptions) {
this.filteredOptions = changes.autocompleteOptions.currentValue.length > 0 ? this.filter(changes.autocompleteOptions.currentValue, this.formCtrl.value) : [];
}
} }
ngOnDestroy() { ngOnDestroy() {
@@ -96,15 +122,20 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
} }
if (value && this.isExists(value) && !this.isAdded(value)) { if (value && this.isExists(value) && !this.isAdded(value)) {
this.selectedOptions.push(value); if (this.allowOnlyPredefinedValues) {
const index = this.autocompleteOptions.findIndex(option => option.value.toLowerCase() === value.toLowerCase());
this.selectedOptions.push(this.autocompleteOptions[index]);
} else {
this.selectedOptions.push({value});
}
this.optionsChanged.emit(this.selectedOptions); this.optionsChanged.emit(this.selectedOptions);
event.chipInput.clear(); event.chipInput.clear();
this.formCtrl.setValue(null); this.formCtrl.setValue('');
} }
} }
} }
remove(value: string) { remove(value: AutocompleteOption) {
const index = this.selectedOptions.indexOf(value); const index = this.selectedOptions.indexOf(value);
if (index >= 0) { if (index >= 0) {
@@ -114,28 +145,28 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
} }
selected(event: MatAutocompleteSelectedEvent) { selected(event: MatAutocompleteSelectedEvent) {
if (!this.isAdded(event.option.viewValue)) { this.selectedOptions.push(event.option.value);
this.selectedOptions.push(event.option.viewValue); this.optionInput.nativeElement.value = '';
this.optionInput.nativeElement.value = ''; this.formCtrl.setValue('');
this.formCtrl.setValue(null); this.optionsChanged.emit(this.selectedOptions);
this.optionsChanged.emit(this.selectedOptions);
}
} }
private isAdded(value: string): boolean { private isAdded(value: string): boolean {
return this.selectedOptions.includes(value); const valueLowerCase = value.toLowerCase();
return this.selectedOptions.some(option => option.value.toLowerCase() === valueLowerCase);
} }
private isExists(value: string): boolean { private isExists(value: string): boolean {
const valueLowerCase = value.toLowerCase();
return this.allowOnlyPredefinedValues return this.allowOnlyPredefinedValues
? this.autocompleteOptions.map(option => option.toLowerCase()).includes(value.toLowerCase()) ? this.autocompleteOptions.some(option => option.value.toLowerCase() === valueLowerCase)
: true; : true;
} }
private reset() { private reset() {
this.selectedOptions = []; this.selectedOptions = [];
this.optionsChanged.emit(this.selectedOptions); this.optionsChanged.emit(this.selectedOptions);
this.formCtrl.setValue(null); this.formCtrl.setValue('');
this.optionInput.nativeElement.value = ''; this.optionInput.nativeElement.value = '';
} }
} }

View File

@@ -1,7 +1,9 @@
<adf-search-chip-autocomplete-input <adf-search-chip-autocomplete-input
[autocompleteOptions]="autocompleteOptions" [autocompleteOptions]="autocompleteOptions$ | async"
[onReset$]="reset$" [onReset$]="reset$"
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues" [allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
(inputChanged)="onInputChange($event)"
[compareOption]="optionComparator"
(optionsChanged)="onOptionsChange($event)"> (optionsChanged)="onOptionsChange($event)">
</adf-search-chip-autocomplete-input> </adf-search-chip-autocomplete-input>

View File

@@ -22,6 +22,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component'; import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
import { TagService } from '@alfresco/adf-content-services'; import { TagService } from '@alfresco/adf-content-services';
import { EMPTY, of } from 'rxjs'; import { EMPTY, of } from 'rxjs';
import { AutocompleteField } from '../../models/autocomplete-option.interface';
describe('SearchFilterAutocompleteChipsComponent', () => { describe('SearchFilterAutocompleteChipsComponent', () => {
let component: SearchFilterAutocompleteChipsComponent; let component: SearchFilterAutocompleteChipsComponent;
@@ -51,7 +52,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
} as any; } as any;
component.settings = { component.settings = {
field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false, field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false,
options: ['option1', 'option2'] autocompleteOptions: [{value: 'option1'}, {value: 'option2'}]
}; };
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -63,13 +64,16 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
} }
it('should set autocomplete options on init', () => { it('should set autocomplete options on init', (done) => {
component.settings.options = ['test 1', 'test 2']; component.settings.autocompleteOptions = [{value: 'test 1'}, {value: 'test 2'}];
component.ngOnInit(); component.ngOnInit();
expect(component.autocompleteOptions).toEqual(['test 1', 'test 2']); component.autocompleteOptions$.subscribe(result => {
expect(result).toEqual([{value: 'test 1'}, {value: 'test 2'}]);
done();
});
}); });
it('should load tags if field = TAG', () => { it('should load tags if field = TAG', (done) => {
const tagPagingMock = { const tagPagingMock = {
list: { list: {
pagination: {}, pagination: {},
@@ -77,10 +81,13 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
} }
}; };
component.settings.field = 'TAG'; component.settings.field = AutocompleteField.TAG;
spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock)); spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock));
component.ngOnInit(); component.ngOnInit();
expect(component.autocompleteOptions).toEqual(['tag1', 'tag2']); component.autocompleteOptions$.subscribe(result => {
expect(result).toEqual([{value: 'tag1'},{value: 'tag2'}]);
done();
});
}); });
it('should update display value when options changes', () => { it('should update display value when options changes', () => {
@@ -94,9 +101,9 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
}); });
it('should reset value and display value when reset button is clicked', () => { it('should reset value and display value when reset button is clicked', () => {
component.setValue(['option1', 'option2']); component.setValue([{value: 'option1'}, {value: 'option2'}]);
fixture.detectChanges(); fixture.detectChanges();
expect(component.selectedOptions).toEqual(['option1', 'option2']); expect(component.selectedOptions).toEqual([{value: 'option1'}, {value: 'option2'}]);
spyOn(component.context, 'update'); spyOn(component.context, 'update');
spyOn(component.displayValue$, 'next'); spyOn(component.displayValue$, 'next');
const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement; const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement;
@@ -110,13 +117,18 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
it('should correctly compose the search query', () => { it('should correctly compose the search query', () => {
spyOn(component.context, 'update'); spyOn(component.context, 'update');
addNewOption('option2'); component.selectedOptions = [{value: 'option2'}, {value: 'option1'}];
addNewOption('option1');
const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement; const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement;
applyBtn.click(); applyBtn.click();
fixture.detectChanges(); fixture.detectChanges();
expect(component.context.update).toHaveBeenCalled(); expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('test: "option2" OR test: "option1"'); expect(component.context.queryFragments[component.id]).toBe('test:"option2" OR test:"option1"');
component.settings.field = AutocompleteField.CATEGORIES;
component.selectedOptions = [{id: 'test-id', value: 'test'}];
applyBtn.click();
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe('cm:categories:"workspace://SpacesStore/test-id"');
}); });
}); });

View File

@@ -16,12 +16,14 @@
*/ */
import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { SearchWidget } from '../../models/search-widget.interface'; import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { SearchFilterList } from '../../models/search-filter-list.model'; import { SearchFilterList } from '../../models/search-filter-list.model';
import { TagService } from '../../../tag/services/tag.service'; import { TagService } from '../../../tag/services/tag.service';
import { CategoryService } from '../../../category/services/category.service';
import { AutocompleteField, AutocompleteOption } from '../../models/autocomplete-option.interface';
@Component({ @Component({
selector: 'adf-search-filter-autocomplete-chips', selector: 'adf-search-filter-autocomplete-chips',
@@ -32,18 +34,19 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
id: string; id: string;
settings?: SearchWidgetSettings; settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService; context?: SearchQueryBuilderService;
options: SearchFilterList<string[]>; options: SearchFilterList<AutocompleteOption[]>;
startValue: string[] = null; startValue: AutocompleteOption[] = [];
displayValue$ = new Subject<string>(); displayValue$ = new Subject<string>();
selectedOptions: AutocompleteOption[] = [];
enableChangeUpdate: boolean;
private resetSubject$ = new Subject<void>(); private resetSubject$ = new Subject<void>();
reset$: Observable<void> = this.resetSubject$.asObservable(); reset$: Observable<void> = this.resetSubject$.asObservable();
autocompleteOptions: string[] = []; private autocompleteOptionsSubject$ = new BehaviorSubject<AutocompleteOption[]>([]);
selectedOptions: string[] = []; autocompleteOptions$: Observable<AutocompleteOption[]> = this.autocompleteOptionsSubject$.asObservable();
enableChangeUpdate: boolean;
constructor( private tagService: TagService ) { constructor(private tagService: TagService, private categoryService: CategoryService) {
this.options = new SearchFilterList<string[]>(); this.options = new SearchFilterList<AutocompleteOption[]>();
} }
ngOnInit() { ngOnInit() {
@@ -70,11 +73,11 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
return !!this.selectedOptions; return !!this.selectedOptions;
} }
getCurrentValue(): string[]{ getCurrentValue(): AutocompleteOption[] {
return this.selectedOptions; return this.selectedOptions;
} }
onOptionsChange(selectedOptions: string[]) { onOptionsChange(selectedOptions: AutocompleteOption[]) {
this.selectedOptions = selectedOptions; this.selectedOptions = selectedOptions;
if (this.enableChangeUpdate) { if (this.enableChangeUpdate) {
this.updateQuery(); this.updateQuery();
@@ -82,27 +85,62 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
} }
} }
setValue(value: string[]) { setValue(value: AutocompleteOption[]) {
this.selectedOptions = value; this.selectedOptions = value;
this.displayValue$.next(this.selectedOptions.join(', ')); this.displayValue$.next(this.selectedOptions.join(', '));
this.submitValues(); this.submitValues();
} }
onInputChange(value: string) {
if (this.settings.field === AutocompleteField.CATEGORIES && value) {
this.searchForExistingCategories(value);
}
}
optionComparator(option1: AutocompleteOption, option2: AutocompleteOption): boolean {
return option1.id
? option1.id.toUpperCase() === option2.id.toUpperCase()
: option1.value.toUpperCase() === option2.value.toUpperCase();
}
private updateQuery() { private updateQuery() {
this.displayValue$.next(this.selectedOptions.join(', ')); this.displayValue$.next(this.selectedOptions.map(option => option.value).join(', '));
if (this.context && this.settings && this.settings.field) { if (this.context && this.settings && this.settings.field) {
this.context.queryFragments[this.id] = this.selectedOptions.map(val => `${this.settings.field}: "${val}"`).join(' OR '); let queryFragments;
if (this.settings.field === AutocompleteField.CATEGORIES) {
queryFragments = this.selectedOptions.map(val => `${this.settings.field}:"workspace://SpacesStore/${val.id}"`);
} else {
queryFragments = this.selectedOptions.map(val => `${this.settings.field}:"${val.value}"`);
}
this.context.queryFragments[this.id] = queryFragments.join(' OR ');
this.context.update(); this.context.update();
} }
} }
private setOptions() { private setOptions() {
if (this.settings.field === 'TAG') { switch (this.settings.field) {
this.tagService.getAllTheTags().subscribe(res => { case AutocompleteField.TAG:
this.autocompleteOptions = res.list.entries.map(tag => tag.entry.tag); this.tagService.getAllTheTags().subscribe(tagPaging => {
}); this.autocompleteOptionsSubject$.next(tagPaging.list.entries.map(tag => ({
} else { value: tag.entry.tag
this.autocompleteOptions = this.settings.options; })));
});
break;
case AutocompleteField.CATEGORIES:
this.autocompleteOptionsSubject$.next([]);
break;
default:
this.autocompleteOptionsSubject$.next(this.settings.autocompleteOptions);
} }
} }
private searchForExistingCategories(searchTerm: string) {
this.categoryService.searchCategories(searchTerm, 0, 15).subscribe((existingCategoriesResult) => {
this.autocompleteOptionsSubject$.next(existingCategoriesResult.list.entries.map((rowEntry) => {
const path = rowEntry.entry.path.name.split('/').splice(3).join('/');
const fullPath = path ? `${path}/${rowEntry.entry.name}` : rowEntry.entry.name;
return {id: rowEntry.entry.id, value: rowEntry.entry.name, fullPath};
}));
});
}
} }

View File

@@ -42,7 +42,7 @@
</mat-form-field> </mat-form-field>
<p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p> <p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p>
<adf-search-chip-autocomplete-input <adf-search-chip-autocomplete-input
[autocompleteOptions]="settings?.fileExtensions" [autocompleteOptions]="autocompleteOptions"
(optionsChanged)="selectedExtensions = $event" (optionsChanged)="selectedExtensions = $event"
[onReset$]="reset$" [onReset$]="reset$"
[allowOnlyPredefinedValues]="false" [allowOnlyPredefinedValues]="false"

View File

@@ -140,9 +140,10 @@ describe('SearchPropertiesComponent', () => {
field: 'field', field: 'field',
fileExtensions: ['pdf', 'doc', 'txt'] fileExtensions: ['pdf', 'doc', 'txt']
}; };
component.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
expect(searchChipAutocompleteInputComponent.autocompleteOptions).toBe(component.settings.fileExtensions); expect(searchChipAutocompleteInputComponent.autocompleteOptions).toEqual([{value: 'pdf'}, {value: 'doc'}, {value: 'txt'}]);
}); });
it('should set onReset$ for SearchChipAutocompleteInputComponent to correct value', () => { it('should set onReset$ for SearchChipAutocompleteInputComponent to correct value', () => {
@@ -154,10 +155,10 @@ describe('SearchPropertiesComponent', () => {
}); });
it('should compare file extensions case insensitive after calling compareOption on SearchChipAutocompleteInputComponent', () => { it('should compare file extensions case insensitive after calling compareOption on SearchChipAutocompleteInputComponent', () => {
const option1 = 'pdf'; const option1 = {value: 'pdf'};
const option2 = 'PdF'; const option2 = {value: 'PdF'};
expect(searchChipAutocompleteInputComponent.compareOption(option1, option2)).toBeTrue(); expect(searchChipAutocompleteInputComponent.compareOption(option1, option2)).toBeTrue();
expect(searchChipAutocompleteInputComponent.compareOption(option1, `${option2}1`)).toBeFalse(); expect(searchChipAutocompleteInputComponent.compareOption(option1, {value: `${option2.value}1`})).toBeFalse();
}); });
it('should remove preceding dot after calling formatChipValue on SearchChipAutocompleteInputComponent', () => { it('should remove preceding dot after calling formatChipValue on SearchChipAutocompleteInputComponent', () => {
@@ -167,11 +168,11 @@ describe('SearchPropertiesComponent', () => {
}); });
it('should filter file extensions case insensitive without dots after calling filter on SearchChipAutocompleteInputComponent', () => { it('should filter file extensions case insensitive without dots after calling filter on SearchChipAutocompleteInputComponent', () => {
const extensions = ['pdf', 'jpg', 'txt', 'png']; const extensions = [{value: 'pdf'}, {value: 'jpg'}, {value: 'txt'}, {value: 'png'}];
const searchValue = 'p'; const searchValue = 'p';
expect(searchChipAutocompleteInputComponent.filter(extensions, searchValue)).toEqual(['pdf', 'jpg', 'png']); expect(searchChipAutocompleteInputComponent.filter(extensions, searchValue)).toEqual([{value:'pdf'}, {value:'jpg'}, {value:'png'}]);
expect(searchChipAutocompleteInputComponent.filter(extensions, `.${searchValue}`)).toEqual(['pdf', 'png']); expect(searchChipAutocompleteInputComponent.filter(extensions, `.${searchValue}`)).toEqual([{value:'pdf'}, {value:'png'}]);
}); });
it('should set placeholder for SearchChipAutocompleteInputComponent to correct value', () => { it('should set placeholder for SearchChipAutocompleteInputComponent to correct value', () => {
@@ -259,17 +260,17 @@ describe('SearchPropertiesComponent', () => {
}); });
it('should search by single file type', () => { it('should search by single file type', () => {
const extension = 'pdf'; const extension = {value: 'pdf'};
getSearchChipAutocompleteInputComponent().optionsChanged.emit([extension]); getSearchChipAutocompleteInputComponent().optionsChanged.emit([extension]);
component.submitValues(); component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith(extension); expect(component.displayValue$.next).toHaveBeenCalledWith('pdf');
expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension}")`); expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension.value}")`);
expect(component.context.update).toHaveBeenCalled(); expect(component.context.update).toHaveBeenCalled();
}); });
it('should search by multiple file types', () => { it('should search by multiple file types', () => {
getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']); getSearchChipAutocompleteInputComponent().optionsChanged.emit([{value:'pdf'}, {value:'txt'}]);
component.submitValues(); component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt'); expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt');
@@ -279,7 +280,7 @@ describe('SearchPropertiesComponent', () => {
it('should search by file size and type', () => { it('should search by file size and type', () => {
typeInFileSizeInput(); typeInFileSizeInput();
getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']); getSearchChipAutocompleteInputComponent().optionsChanged.emit([{value:'pdf'}, {value:'txt'}]);
component.submitValues(); component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB, pdf, txt'); expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB, pdf, txt');
@@ -315,7 +316,7 @@ describe('SearchPropertiesComponent', () => {
clickFileSizeUnitsSelect(); clickFileSizeUnitsSelect();
getSelectOptions()[1].nativeElement.click(); getSelectOptions()[1].nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
const extensions = ['pdf', 'txt']; const extensions = [{value: 'pdf'}, {value: 'txt'}];
getSearchChipAutocompleteInputComponent().optionsChanged.emit(extensions); getSearchChipAutocompleteInputComponent().optionsChanged.emit(extensions);
expect(component.getCurrentValue()).toEqual({ expect(component.getCurrentValue()).toEqual({
@@ -324,7 +325,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.MB, fileSizeUnit: FileSizeUnit.MB,
fileSizeOperator: FileSizeOperator.AT_MOST fileSizeOperator: FileSizeOperator.AT_MOST
}, },
fileExtensions: extensions fileExtensions: ['pdf', 'txt']
}); });
}); });
}); });
@@ -342,7 +343,7 @@ describe('SearchPropertiesComponent', () => {
getSelectOptions()[1].nativeElement.click(); getSelectOptions()[1].nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent(); searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent();
searchChipAutocompleteInputComponent.optionsChanged.emit(['pdf', 'txt']); searchChipAutocompleteInputComponent.optionsChanged.emit([{value: 'pdf'}, {value: 'txt'}]);
}); });
it('should reset form', () => { it('should reset form', () => {

View File

@@ -26,6 +26,7 @@ import { SearchQueryBuilderService } from '../../services/search-query-builder.s
import { SearchProperties } from './search-properties'; import { SearchProperties } from './search-properties';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { SearchWidget } from '../../models/search-widget.interface'; import { SearchWidget } from '../../models/search-widget.interface';
import { AutocompleteOption } from '../../models/autocomplete-option.interface';
@Component({ @Component({
selector: 'adf-search-properties', selector: 'adf-search-properties',
@@ -39,6 +40,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
context?: SearchQueryBuilderService; context?: SearchQueryBuilderService;
startValue: SearchProperties; startValue: SearchProperties;
displayValue$ = new Subject<string>(); displayValue$ = new Subject<string>();
autocompleteOptions: AutocompleteOption[] = [];
private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({ private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({
fileSizeOperator: FileSizeOperator.AT_LEAST, fileSizeOperator: FileSizeOperator.AT_LEAST,
@@ -77,8 +79,8 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
return this._reset$; return this._reset$;
} }
set selectedExtensions(extensions: string[]) { set selectedExtensions(extensions: AutocompleteOption[]) {
this._selectedExtensions = extensions; this._selectedExtensions = this.parseFromAutocompleteOptions(extensions);
} }
constructor(private formBuilder: FormBuilder, private translateService: TranslateService) {} constructor(private formBuilder: FormBuilder, private translateService: TranslateService) {}
@@ -88,6 +90,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
if (!this.settings.fileExtensions) { if (!this.settings.fileExtensions) {
this.settings.fileExtensions = []; this.settings.fileExtensions = [];
} }
this.autocompleteOptions = this.parseToAutocompleteOptions(this.settings.fileExtensions);
[this.sizeField, this.nameField] = this.settings.field.split(','); [this.sizeField, this.nameField] = this.settings.field.split(',');
} }
if (this.startValue) { if (this.startValue) {
@@ -127,8 +130,8 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
return event.key !== '-' && event.key !== 'e' && event.key !== '+'; return event.key !== '-' && event.key !== 'e' && event.key !== '+';
} }
compareFileExtensions(extension1: string, extension2: string): boolean { compareFileExtensions(extension1: AutocompleteOption, extension2: AutocompleteOption): boolean {
return extension1.toUpperCase() === extension2.toUpperCase(); return extension1.value.toUpperCase() === extension2.value.toUpperCase();
} }
getExtensionWithoutDot(extension: string): string { getExtensionWithoutDot(extension: string): string {
@@ -136,11 +139,11 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
return extensionSplitByDot[extensionSplitByDot.length - 1]; return extensionSplitByDot[extensionSplitByDot.length - 1];
} }
filterExtensions = (extensions: string[], filterValue: string): string[] => { filterExtensions = (extensions: AutocompleteOption[], filterValue: string): AutocompleteOption[] => {
const filterValueLowerCase = this.getExtensionWithoutDot(filterValue).toLowerCase(); const filterValueLowerCase = this.getExtensionWithoutDot(filterValue).toLowerCase();
const extensionWithDot = filterValue.startsWith('.'); const extensionWithDot = filterValue.startsWith('.');
return extensions.filter((option) => { return extensions.filter((option) => {
const optionLowerCase = option.toLowerCase(); const optionLowerCase = option.value.toLowerCase();
return extensionWithDot && filterValueLowerCase ? optionLowerCase.startsWith(filterValueLowerCase) : optionLowerCase.includes(filterValue); return extensionWithDot && filterValueLowerCase ? optionLowerCase.startsWith(filterValueLowerCase) : optionLowerCase.includes(filterValue);
}); });
}; };
@@ -196,10 +199,18 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
setValue(searchProperties: SearchProperties) { setValue(searchProperties: SearchProperties) {
this.form.patchValue(searchProperties.fileSizeCondition); this.form.patchValue(searchProperties.fileSizeCondition);
this.selectedExtensions = searchProperties.fileExtensions; this.selectedExtensions = this.parseToAutocompleteOptions(searchProperties.fileExtensions);
this.submitValues(); this.submitValues();
} }
private parseToAutocompleteOptions(array: string[]): AutocompleteOption[] {
return array.map(value => ({value}));
}
private parseFromAutocompleteOptions(array: AutocompleteOption[]): string[] {
return array.flatMap(option => option.value);
}
private getOperatorNameWidth(operator: string, font: string): number { private getOperatorNameWidth(operator: string, font: string): number {
const context = this.canvas.getContext('2d'); const context = this.canvas.getContext('2d');
context.font = font; context.font = font;

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.
*/
export interface AutocompleteOption {
value: string;
id?: string;
fullPath?: string;
}
export enum AutocompleteField {
TAG = 'TAG',
CATEGORIES = 'cm:categories'
}

View File

@@ -15,6 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { AutocompleteOption } from './autocomplete-option.interface';
export interface SearchWidgetSettings { export interface SearchWidgetSettings {
field: string; field: string;
/* allow the user to update search in every change */ /* allow the user to update search in every change */
@@ -27,6 +29,8 @@ export interface SearchWidgetSettings {
format?: string; format?: string;
/* allow the user to search only within predefined options */ /* allow the user to search only within predefined options */
allowOnlyPredefinedValues?: boolean; allowOnlyPredefinedValues?: boolean;
/* allow the user to predefine autocomplete options */
autocompleteOptions?: AutocompleteOption[];
[indexer: string]: any; [indexer: string]: any;
} }