();
+
+ 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 = '';
+ }
+}
diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html
new file mode 100644
index 0000000000..7e95495bd0
--- /dev/null
+++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts
new file mode 100644
index 0000000000..8d3d17b342
--- /dev/null
+++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts
@@ -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;
+ 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"');
+ });
+});
diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts
new file mode 100644
index 0000000000..09eb499474
--- /dev/null
+++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts
@@ -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;
+ startValue: string[] = null;
+ displayValue$ = new Subject();
+
+ private resetSubject$ = new Subject();
+ reset$: Observable = this.resetSubject$.asObservable();
+ autocompleteOptions: string[] = [];
+ selectedOptions: string[] = [];
+ enableChangeUpdate: boolean;
+
+ constructor( private tagService: TagService ) {
+ this.options = new SearchFilterList();
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts
index 5893754ed4..4885fd2bbf 100644
--- a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts
+++ b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts
@@ -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;
}
diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts
index 9c799db124..9fa4f9e752 100644
--- a/lib/content-services/src/lib/search/public-api.ts
+++ b/lib/content-services/src/lib/search/public-api.ts
@@ -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';
diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts
index 0cc7f1ab0b..b28271cb9d 100644
--- a/lib/content-services/src/lib/search/search.module.ts
+++ b/lib/content-services/src/lib/search/search.module.ts
@@ -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,
diff --git a/lib/content-services/src/lib/search/services/search-filter.service.ts b/lib/content-services/src/lib/search/services/search-filter.service.ts
index 70b5fa1444..9ffe6cc671 100644
--- a/lib/content-services/src/lib/search/services/search-filter.service.ts
+++ b/lib/content-services/src/lib/search/services/search-filter.service.ts
@@ -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
};
}