diff --git a/docs/content-services/components/search-facet-chip-tabbed.md b/docs/content-services/components/search-facet-chip-tabbed.md new file mode 100644 index 0000000000..008871074b --- /dev/null +++ b/docs/content-services/components/search-facet-chip-tabbed.md @@ -0,0 +1,49 @@ +--- +Title: Search facet chip tabbed component +Added: v6.2.0 +Status: Active +Last reviewed: 2023-07-18 +--- + +# [Search facet chip tabbed component](../../../lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.ts "Defined in search-facet-chip-tabbed.component.ts") + +Implements a [facet widget](../../../lib/content-services/src/lib/search/models/facet-widget.interface.ts) consisting of creator and modifier facets inside tabbed component. + +![Search facet chip tabbed](../../docassets/images/search-facet-chip-tabbed.png) + +## Basic usage +When both creator and modifier facets are present in config file as stated below they will be merged into this component. + +```json +{ + "search": { + "facetFields": { + "fields": [ + { + "mincount": 1, + "field": "creator", + "label": "SEARCH.FACET_FIELDS.CREATOR", + }, + { + "mincount": 1, + "field": "modifier", + "label": "SEARCH.FACET_FIELDS.MODIFIER", + } + ] + } + } +} +``` + +### Settings + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tabbedFacet | [TabbedFacetField](../../../lib/content-services/src/lib/search/models/tabbed-facet-field.interface.ts) | Tabbed facet configuration containing label, fields and facets to display. Required value | + +## 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) diff --git a/docs/docassets/images/search-facet-chip-tabbed.png b/docs/docassets/images/search-facet-chip-tabbed.png new file mode 100644 index 0000000000..d48babab5d Binary files /dev/null and b/docs/docassets/images/search-facet-chip-tabbed.png differ diff --git a/docs/versionIndex.md b/docs/versionIndex.md index b7940419cd..887aa2fd8b 100644 --- a/docs/versionIndex.md +++ b/docs/versionIndex.md @@ -51,6 +51,7 @@ backend services have been tested with each released version of ADF. - [Search Date Range Advanced Component](content-services/components/search-date-range-advanced.component.md) - [Search Date Range Advanced Tabbed Component](content-services/components/search-date-range-advanced-tabbed.component.md) - [Search Filter Tabbed Component](content-services/components/search-filter-tabbed.component.md) +- [Search Facet Chip Tabbed Component](content-services/components/search-facet-chip-tabbed.md) diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 8162a3d4f1..07635f268a 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -336,7 +336,12 @@ "SEARCH_FILTER": "Search Filter List", "OPTIONS-SELECTION": "Options Selection" }, - "ANY": "Any" + "ANY": "Any", + "PEOPLE": "People" + }, + "FACET_FIELDS": { + "MODIFIER_LABEL": "Modified by", + "CREATOR_LABEL": "Created by" }, "ICONS": { "ft_ic_raster_image": "Image file", diff --git a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts index 45666f4077..6098b3ef73 100644 --- a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts +++ b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts @@ -105,7 +105,7 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy, ngOnChanges(changes: SimpleChanges) { if (changes.autocompleteOptions) { - this.filteredOptions = changes.autocompleteOptions.currentValue.length > 0 ? this.filter(changes.autocompleteOptions.currentValue, this.formCtrl.value) : []; + this.filteredOptions = changes.autocompleteOptions.currentValue?.length > 0 ? this.filter(changes.autocompleteOptions.currentValue, this.formCtrl.value) : []; } } diff --git a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts index 7e4d01cfa7..687dd795a1 100644 --- a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.spec.ts @@ -54,15 +54,15 @@ describe('SearchFacetFieldComponent', () => { spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough(); const event: any = { checked: true }; - const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; + const facetField: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 }; - component.field = field; + component.field = facetField; fixture.detectChanges(); - component.onToggleBucket(event, field, bucket); + component.onToggleBucket(event, facetField, bucket); expect(bucket.checked).toBeTruthy(); - expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(field, bucket); + expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(facetField.field, bucket); expect(queryBuilder.update).toHaveBeenCalled(); expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); }); @@ -72,15 +72,15 @@ describe('SearchFacetFieldComponent', () => { spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); const event: any = { checked: false }; - const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; + const facetField: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() }; const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 }; - component.field = field; + component.field = facetField; fixture.detectChanges(); - component.onToggleBucket(event, field, bucket); + component.onToggleBucket(event, facetField, bucket); - expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, bucket); + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(facetField.field, bucket); expect(queryBuilder.update).toHaveBeenCalled(); expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); }); @@ -91,15 +91,15 @@ describe('SearchFacetFieldComponent', () => { const event: any = { checked: false }; const query = { checked: true, label: 'q1', filterQuery: 'query1' }; - const field = { field: 'q1', type: 'query', label: 'label1', buckets: new SearchFilterList([ query ] ) } as FacetField; + const facetField = { field: 'q1', type: 'query', label: 'label1', buckets: new SearchFilterList([ query ] ) } as FacetField; - component.field = field; + component.field = facetField; fixture.detectChanges(); - component.onToggleBucket(event, field, query as any); + component.onToggleBucket(event, facetField, query as any); expect(query.checked).toEqual(false); - expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, query); + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(facetField.field, query); expect(queryBuilder.update).toHaveBeenCalled(); expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled(); }); diff --git a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts index 568e74bb3f..d81e6a42ec 100644 --- a/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts +++ b/lib/content-services/src/lib/search/components/search-facet-field/search-facet-field.component.ts @@ -61,7 +61,7 @@ export class SearchFacetFieldComponent implements FacetWidget { selectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { if (bucket) { bucket.checked = true; - this.queryBuilder.addUserFacetBucket(field, bucket); + this.queryBuilder.addUserFacetBucket(field.field, bucket); this.searchFacetFiltersService.updateSelectedBuckets(); if (this.canUpdateOnChange) { this.updateDisplayValue(); @@ -73,7 +73,7 @@ export class SearchFacetFieldComponent implements FacetWidget { unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { if (bucket) { bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); + this.queryBuilder.removeUserFacetBucket(field.field, bucket); this.searchFacetFiltersService.updateSelectedBuckets(); if (this.canUpdateOnChange) { this.updateDisplayValue(); @@ -93,7 +93,7 @@ export class SearchFacetFieldComponent implements FacetWidget { if (field && field.buckets) { for (const bucket of field.buckets.items) { bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); + this.queryBuilder.removeUserFacetBucket(field.field, bucket); } this.searchFacetFiltersService.updateSelectedBuckets(); if (this.canUpdateOnChange) { diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.html new file mode 100644 index 0000000000..0e85bd9186 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.html @@ -0,0 +1,59 @@ + + + {{ tabbedFacet.label | translate }}: + + +   {{ displayValue | translate }} + +  {{ 'SEARCH.FILTER.ANY' | translate }} + {{ chipIcon }} + + remove + + + + +
+ + + {{ tabbedFacet.label | translate }} + + + + + + + + + + + + + + +
+
diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.scss b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.scss new file mode 100644 index 0000000000..fcc7c885c3 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.scss @@ -0,0 +1,17 @@ +adf-search-facet-chip-tabbed { + .adf-search-filter-chip-tabbed { + &[disabled] { + pointer-events: none; + } + } + + .adf-search-widget-extra-width { + max-width: 500px; + } +} + +adf-search-filter-tabbed { + .mat-tab-body-wrapper { + margin-top: 16px; + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.spec.ts new file mode 100644 index 0000000000..1ad66e4f6b --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.spec.ts @@ -0,0 +1,232 @@ +/*! + * @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 { ContentTestingModule } from '../../../../testing/content.testing.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { SearchQueryBuilderService } from '../../../services/search-query-builder.service'; +import { SearchFilterList } from '../../../models/search-filter-list.model'; +import { SearchFacetChipTabbedComponent } from './search-facet-chip-tabbed.component'; +import { FacetField } from '../../../models/facet-field.interface'; +import { SearchFacetFiltersService } from '../../../services/search-facet-filters.service'; +import { SimpleChange } from '@angular/core'; + +describe('SearchFacetChipTabbedComponent', () => { + let component: SearchFacetChipTabbedComponent; + let fixture: ComponentFixture; + let queryBuilder: SearchQueryBuilderService; + let searchFacetService: SearchFacetFiltersService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), ContentTestingModule] + }); + fixture = TestBed.createComponent(SearchFacetChipTabbedComponent); + component = fixture.componentInstance; + queryBuilder = TestBed.inject(SearchQueryBuilderService); + searchFacetService = TestBed.inject(SearchFacetFiltersService); + spyOn(queryBuilder, 'update').and.stub(); + + const facet1: FacetField = { type: 'field', label: 'field', field: 'field', buckets: new SearchFilterList() }; + const facet2: FacetField = { type: 'field', label: 'field2', field: 'field2', buckets: new SearchFilterList() }; + + component.tabbedFacet = { + fields: ['field', 'field2'], + label: 'LABEL', + facets: { + field: facet1, + field2: facet2 + } + }; + fixture.detectChanges(); + }); + + function openFacet() { + const chip = fixture.debugElement.query(By.css('mat-chip')); + chip.triggerEventHandler('click', {}); + fixture.detectChanges(); + } + + function getDisplayValue(): string { + return fixture.debugElement.query(By.css('.adf-search-filter-ellipsis.adf-filter-value')).nativeElement.innerText.trim(); + } + + function getTabs(): HTMLDivElement[] { + return fixture.debugElement.queryAll(By.css('.mat-tab-label-content')).map((element) => element.nativeElement); + } + + function changeTab(tabIndex: number) { + getTabs()[tabIndex].click(); + fixture.detectChanges(); + } + + function triggerComponentChanges() { + component.ngOnChanges({ + tabbedFacet: new SimpleChange(null, component.tabbedFacet, false) + }); + fixture.detectChanges(); + } + + function addBucketItem(field: string, displayValue: string) { + component.tabbedFacet.facets[field].buckets.items.push({ + count: 1, + label: displayValue, + display: displayValue, + filterQuery: '' + }); + triggerComponentChanges(); + } + + it('should display correct label for tabbed facet', () => { + const label = fixture.debugElement.query(By.css('.adf-search-filter-placeholder')).nativeElement.innerText; + expect(label).toBe(component.tabbedFacet.label + ':'); + }); + + it('should display any as display value when nothing is selected', () => { + const displayValue = getDisplayValue(); + expect(displayValue).toBe('SEARCH.FILTER.ANY'); + }); + + it('should display remove icon and disable facet when no items are loaded', () => { + const chip = fixture.debugElement.query(By.css('mat-chip')); + const icon = fixture.debugElement.query(By.css('mat-chip mat-icon')).nativeElement.innerText; + expect(chip.classes['mat-chip-disabled']).toBeTrue(); + expect(icon).toEqual('remove'); + }); + + it('should not open context menu when no items are loaded', () => { + spyOn(component.menuTrigger, 'openMenu'); + const chip = fixture.debugElement.query(By.css('mat-chip')).nativeElement; + chip.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); + expect(component.menuTrigger.openMenu).not.toHaveBeenCalled(); + }); + + it('should display correct title when facet is opened', () => { + openFacet(); + const title = fixture.debugElement.query(By.css('.adf-search-filter-title')).nativeElement.innerText.split('\n')[0]; + expect(title).toBe(component.tabbedFacet.label); + }); + + it('should display 2 tabs with specific labels', () => { + openFacet(); + const tabLabels = getTabs(); + expect(tabLabels.length).toBe(2); + expect(tabLabels[0].innerText).toBe(component.tabbedFacet.facets['field'].label); + expect(tabLabels[1].innerText).toBe(component.tabbedFacet.facets['field2'].label); + }); + + it('should display creator tab as active initially and allow navigation', () => { + openFacet(); + let activeTabLabel = fixture.debugElement.query(By.css('.mat-tab-label-active .mat-tab-label-content')).nativeElement.innerText; + expect(activeTabLabel).toBe(component.tabbedFacet.facets['field'].label); + + changeTab(1); + activeTabLabel = fixture.debugElement.query(By.css('.mat-tab-label-active .mat-tab-label-content')).nativeElement.innerText; + expect(activeTabLabel).toBe(component.tabbedFacet.facets['field2'].label); + }); + + it('should display arrow down icon and not disable the chip when items are loaded', () => { + addBucketItem('field', 'test'); + const chip = fixture.debugElement.query(By.css('mat-chip')); + const icon = fixture.debugElement.query(By.css('mat-chip mat-icon')).nativeElement.innerText; + expect(chip.classes['mat-chip-disabled']).toBeUndefined(); + expect(icon).toEqual('keyboard_arrow_down'); + }); + + it('should display arrow up icon when menu is opened', () => { + addBucketItem('field', 'test'); + openFacet(); + const icon = fixture.debugElement.query(By.css('mat-chip mat-icon')).nativeElement.innerText; + expect(icon).toEqual('keyboard_arrow_up'); + }); + + it('should create empty selected options for each tab initially', () => { + expect(component.selectedOptions['field']).toEqual([]); + expect(component.selectedOptions['field2']).toEqual([]); + }); + + it('should update autocomplete options when buckets change', () => { + addBucketItem('field', 'test'); + addBucketItem('field2', 'test2'); + expect(component.autocompleteOptions['field'].length).toBe(1); + expect(component.autocompleteOptions['field'][0]).toEqual({value: 'test'}); + expect(component.autocompleteOptions['field2'].length).toBe(1); + expect(component.autocompleteOptions['field2'][0]).toEqual({value: 'test2'}); + }); + + it('should add buckets when items are selected', () => { + spyOn(queryBuilder, 'addUserFacetBucket'); + addBucketItem('field', 'test'); + addBucketItem('field2', 'test2'); + component.onOptionsChange([{ value: 'test' }], 'field'); + expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith('field',component.tabbedFacet.facets['field'].buckets.items[0]); + }); + + it('should remove buckets when items are unselected', () => { + spyOn(queryBuilder, 'removeUserFacetBucket'); + addBucketItem('field', 'test'); + addBucketItem('field2', 'test2'); + component.onOptionsChange([], 'field'); + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith('field',component.tabbedFacet.facets['field'].buckets.items[0]); + }); + + it('should update display value when next elements are selected', () => { + const selectedOption1 = 'test'; + const selectedOption2 = 'test2'; + addBucketItem('field', selectedOption1); + addBucketItem('field', selectedOption2); + component.onOptionsChange([{ value: selectedOption1 }, { value: selectedOption2 }],'field'); + fixture.detectChanges(); + expect(getDisplayValue()).toBe(`${component.tabbedFacet.facets['field'].label}_LABEL: ${selectedOption1}, ${selectedOption2}`); + }); + + it('should update display value when elements from both tabs are selected', () => { + const selectedOption1 = 'test'; + const selectedOption2 = 'test2'; + addBucketItem('field', selectedOption1); + addBucketItem('field2', selectedOption2); + component.onOptionsChange([{ value: selectedOption1 }], 'field'); + component.onOptionsChange([{ value: selectedOption2 }], 'field2'); + fixture.detectChanges(); + expect(getDisplayValue()).toBe(`${component.tabbedFacet.facets['field'].label}_LABEL: ${selectedOption1} ${component.tabbedFacet.facets['field2'].label}_LABEL: ${selectedOption2}`); + }); + + it('should update search query and display value when apply btn is clicked', () => { + spyOn(component.menuTrigger, 'closeMenu').and.callThrough(); + spyOn(component, 'updateDisplayValue').and.callThrough(); + spyOn(searchFacetService, 'updateSelectedBuckets').and.callThrough(); + openFacet(); + const applyButton = fixture.debugElement.query(By.css('#apply-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + expect(component.menuTrigger.closeMenu).toHaveBeenCalled(); + expect(component.updateDisplayValue).toHaveBeenCalled(); + expect(searchFacetService.updateSelectedBuckets).toHaveBeenCalled(); + }); + + it('should update search query and display value when cancel btn is clicked', () => { + spyOn(component.menuTrigger, 'closeMenu').and.callThrough(); + spyOn(component, 'updateDisplayValue').and.callThrough(); + openFacet(); + const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button')); + applyButton.triggerEventHandler('click', {}); + expect(queryBuilder.update).toHaveBeenCalled(); + expect(component.menuTrigger.closeMenu).toHaveBeenCalled(); + expect(component.updateDisplayValue).toHaveBeenCalled(); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.ts new file mode 100644 index 0000000000..58e8768aba --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.ts @@ -0,0 +1,174 @@ +/*! + * @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, ElementRef, Inject, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { TabbedFacetField } from '../../../models/tabbed-facet-field.interface'; +import { Subject } from 'rxjs'; +import { SearchQueryBuilderService } from '../../../services/search-query-builder.service'; +import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search-query-service.token'; +import { FacetWidget } from '../../../models/facet-widget.interface'; +import { TranslationService } from '@alfresco/adf-core'; +import { SearchFacetFiltersService } from '../../../services/search-facet-filters.service'; +import { AutocompleteOption } from '../../../models/autocomplete-option.interface'; + +@Component({ + selector: 'adf-search-facet-chip-tabbed', + templateUrl: './search-facet-chip-tabbed.component.html', + styleUrls: ['./search-facet-chip-tabbed.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchFacetChipTabbedComponent implements OnInit, OnChanges, FacetWidget { + @Input() + tabbedFacet: TabbedFacetField; + + @ViewChild('menuContainer', { static: false }) + menuContainer: ElementRef; + + @ViewChild('menuTrigger', { static: false }) + menuTrigger: MatMenuTrigger; + + private resetSubject$ = new Subject(); + + displayValue$ = new Subject(); + reset$ = this.resetSubject$.asObservable(); + focusTrap: ConfigurableFocusTrap; + chipIcon = 'keyboard_arrow_down'; + autocompleteOptions = {}; + selectedOptions = {}; + isPopulated = false; + + constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService, + private translationService: TranslationService, + private searchFacetFiltersService: SearchFacetFiltersService, + private focusTrapFactory: ConfigurableFocusTrapFactory) { + } + + ngOnInit() { + this.tabbedFacet.fields.forEach((field) => { + Object.defineProperty(this.selectedOptions, field, { + value: [], + writable: true + }); + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.tabbedFacet) { + this.isPopulated = this.tabbedFacet.fields.some((field) => this.tabbedFacet.facets[field]?.buckets.items.length > 0); + this.tabbedFacet.fields.forEach((field) => { + const options: AutocompleteOption[] = this.tabbedFacet.facets[field].buckets.items.map((item) => ({ value: item.display })); + Object.defineProperty(this.autocompleteOptions, field, { + value: options, + writable: true + }); + }); + } + } + + onMenuOpen() { + if (this.menuContainer && !this.focusTrap) { + this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement); + } + this.chipIcon = 'keyboard_arrow_up'; + } + + onClosed() { + this.focusTrap.destroy(); + this.focusTrap = null; + this.chipIcon = 'keyboard_arrow_down'; + } + + onRemove() { + this.reset(); + this.menuTrigger.closeMenu(); + } + + onApply() { + this.submitValues(); + this.menuTrigger.closeMenu(); + } + + onEnterKeydown() { + if (this.isPopulated) { + if (!this.menuTrigger.menuOpen) { + this.menuTrigger.openMenu(); + } else { + this.menuTrigger.closeMenu(); + } + } + } + + onEscKeydown() { + if (this.menuTrigger.menuOpen) { + this.menuTrigger.closeMenu(); + } + } + + onOptionsChange(selectedOptions: AutocompleteOption[], field: string) { + this.selectedOptions[field] = selectedOptions.map((selectedOption) => selectedOption.value); + this.isPopulated = this.tabbedFacet.fields.some((facetField) => this.selectedOptions[facetField].length > 0); + this.updateDisplayValue(); + this.updateUserFacetBuckets(); + this.queryBuilder.update(); + } + + updateDisplayValue() { + let displayValue = ''; + this.tabbedFacet.fields.forEach((field) => { + if (this.selectedOptions[field].length > 0) { + const stackedOptions = this.selectedOptions[field].join(', '); + displayValue += `${this.translationService.instant(this.tabbedFacet.facets[field].label + '_LABEL')}: ${stackedOptions} `; + } + }); + this.displayValue$.next(displayValue); + } + + reset() { + this.resetSubject$.next(); + this.updateUserFacetBuckets(); + this.updateDisplayValue(); + this.queryBuilder.update(); + } + + submitValues() { + this.updateUserFacetBuckets(); + this.searchFacetFiltersService.updateSelectedBuckets(); + this.updateDisplayValue(); + this.queryBuilder.update(); + } + + optionComparator(option1: AutocompleteOption, option2: AutocompleteOption): boolean { + return option1.value.toUpperCase() === option2.value.toUpperCase(); + } + + private updateUserFacetBuckets() { + this.tabbedFacet.fields.forEach((field) => { + this.tabbedFacet.facets[field].buckets.items.forEach((item) => { + const matchedOption = this.selectedOptions[field].find((option) => option === item.display); + if (matchedOption) { + item.checked = true; + this.queryBuilder.addUserFacetBucket(field, item); + } else { + item.checked = false; + this.queryBuilder.removeUserFacetBucket(field, item); + } + }); + }); + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html index d9b5dd9dc6..baf944810e 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.html @@ -3,6 +3,12 @@ + + + + diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss index dee5630cb7..3277cbb268 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.scss @@ -1,6 +1,7 @@ @use '@angular/material' as mat; -.adf-search-filter-chip { +.adf-search-filter-chip, +.adf-search-filter-chip-tabbed { &.mat-chip { border: 2px solid transparent; transition: border 500ms ease-in-out; diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts index 12beb81f22..d80a78462a 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.spec.ts @@ -75,7 +75,7 @@ describe('SearchFilterChipsComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()} ]; - searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + searchFacetFiltersService.queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -125,7 +125,7 @@ describe('SearchFilterChipsComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()} ]; - queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -174,7 +174,7 @@ describe('SearchFilterChipsComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() } ]; - queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const data = { list: { context: {} diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts index 00ecad7673..b3052d8cb8 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts @@ -19,6 +19,8 @@ import { Component, Inject, Input, ViewEncapsulation } from '@angular/core'; import { SearchFacetFiltersService } from '../../services/search-facet-filters.service'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token'; import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'adf-search-filter-chips', @@ -27,13 +29,27 @@ import { SearchQueryBuilderService } from '../../services/search-query-builder.s encapsulation: ViewEncapsulation.None }) export class SearchFilterChipsComponent { + private onDestroy$ = new Subject(); + /** Toggles whether to show or not the context facet filters. */ @Input() showContextFacets: boolean = true; + facetChipTabbedId = ''; + constructor( @Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, public facetFiltersService: SearchFacetFiltersService) {} + ngOnInit() { + this.queryBuilder.executed.asObservable() + .pipe(takeUntil(this.onDestroy$)) + .subscribe(() => this.facetChipTabbedId = 'search-fact-chip-tabbed-' + this.facetFiltersService.tabbedFacet?.fields.join('-')); + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } } diff --git a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts index 3980876bd1..8a3fc9291f 100644 --- a/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-filter/search-filter.component.spec.ts @@ -94,7 +94,7 @@ describe('SearchFilterComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) } ]; - queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -138,7 +138,7 @@ describe('SearchFilterComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) } ]; - searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + searchFacetFiltersService.queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const serverResponseFields: any = [ { type: 'field', label: 'f1', field: 'f1', buckets: [ @@ -182,7 +182,7 @@ describe('SearchFilterComponent', () => { { label: 'b2', count: 1, filterQuery: 'filter2' }]) }, { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() } ]; - searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]); + searchFacetFiltersService.queryBuilder.addUserFacetBucket('f1', searchFacetFiltersService.responseFacets[0].buckets.items[0]); const data = { list: { context: {} diff --git a/lib/content-services/src/lib/search/models/tabbed-facet-field.interface.ts b/lib/content-services/src/lib/search/models/tabbed-facet-field.interface.ts new file mode 100644 index 0000000000..52a3391f0c --- /dev/null +++ b/lib/content-services/src/lib/search/models/tabbed-facet-field.interface.ts @@ -0,0 +1,29 @@ +/*! + * @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 { FacetField } from './facet-field.interface'; + +export interface TabbedFacetField { + /* array of fields that tabbed facet will consist of */ + fields: string[]; + /* label to display for tabbed facet */ + label: string; + /* facets to populate tabbed facet tabs */ + facets: { + [propName: string]: FacetField; + }; +} diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts index 52176d8ff1..571797de6d 100644 --- a/lib/content-services/src/lib/search/public-api.ts +++ b/lib/content-services/src/lib/search/public-api.ts @@ -27,6 +27,7 @@ export * from './models/search-configuration.interface'; export * from './services/search-query-builder.service'; export * from './models/search-range.interface'; export * from './models/search-form.interface'; +export * from './models/tabbed-facet-field.interface'; export * from './search-query-service.token'; export * from './services/search-header-query-builder.service'; @@ -67,5 +68,6 @@ export * from './components/search-filter-tabbed/search-filter-tabbed.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 './components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.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 4a90815dd7..af3a9628c7 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -56,6 +56,7 @@ import { SearchFilterTabbedComponent } from './components/search-filter-tabbed/s import { SearchDateRangeAdvancedComponent } from './components/search-date-range-advanced-tabbed/search-date-range-advanced/search-date-range-advanced.component'; import { SearchDateRangeAdvancedTabbedComponent } from './components/search-date-range-advanced-tabbed/search-date-range-advanced-tabbed.component'; import { SearchFilterTabDirective } from './components/search-filter-tabbed/search-filter-tab.directive'; +import { SearchFacetChipTabbedComponent } from './components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component'; @NgModule({ imports: [ @@ -98,7 +99,8 @@ import { SearchFilterTabDirective } from './components/search-filter-tabbed/sear SearchFilterTabbedComponent, SearchDateRangeAdvancedComponent, SearchDateRangeAdvancedTabbedComponent, - SearchFilterTabDirective + SearchFilterTabDirective, + SearchFacetChipTabbedComponent ], exports: [ SearchComponent, @@ -126,7 +128,8 @@ import { SearchFilterTabDirective } from './components/search-filter-tabbed/sear SearchLogicalFilterComponent, SearchFilterTabbedComponent, SearchDateRangeAdvancedComponent, - ResetSearchDirective + ResetSearchDirective, + SearchFacetChipTabbedComponent ], providers: [ { provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService } diff --git a/lib/content-services/src/lib/search/services/base-query-builder.service.ts b/lib/content-services/src/lib/search/services/base-query-builder.service.ts index 38f1576464..c0446f98a0 100644 --- a/lib/content-services/src/lib/search/services/base-query-builder.service.ts +++ b/lib/content-services/src/lib/search/services/base-query-builder.service.ts @@ -183,14 +183,14 @@ export abstract class BaseQueryBuilderService { * @param field The target field * @param bucket Bucket to add */ - addUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) { - if (field && field.field && bucket) { - const buckets = this.userFacetBuckets[field.field] || []; + addUserFacetBucket(field: string, bucket: FacetFieldBucket) { + if (field && bucket) { + const buckets = this.userFacetBuckets[field] || []; const existing = buckets.find((facetBucket) => facetBucket.label === bucket.label); if (!existing) { buckets.push(bucket); } - this.userFacetBuckets[field.field] = buckets; + this.userFacetBuckets[field] = buckets; } } @@ -210,10 +210,10 @@ export abstract class BaseQueryBuilderService { * @param field The target field * @param bucket Bucket to remove */ - removeUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) { - if (field && field.field && bucket) { - const buckets = this.userFacetBuckets[field.field] || []; - this.userFacetBuckets[field.field] = buckets + removeUserFacetBucket(field: string, bucket: FacetFieldBucket) { + if (field && bucket) { + const buckets = this.userFacetBuckets[field] || []; + this.userFacetBuckets[field] = buckets .filter((facetBucket) => facetBucket.label !== bucket.label); } } diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts index c04abefdd9..d67529f0a8 100644 --- a/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts @@ -497,7 +497,7 @@ describe('SearchFacetFiltersService', () => { }] } ]; - let data = { + const data = { list: { context: { facets: fields @@ -513,6 +513,55 @@ describe('SearchFacetFiltersService', () => { expect(searchFacetFiltersService.responseFacets.length).toEqual(2); }); + it('should extract creator and modifier facets and create tabbed facet for them', () => { + searchFacetFiltersService.responseFacets = null; + queryBuilder.config = { + categories: [], + facetFields: { fields: [ + { label: 'creator', field: 'creator' }, + { label: 'modifier', field: 'modifier' } + ]}, + facetQueries: { + queries: [] + } + }; + + const serverResponseFields: any = [ + { + type: 'field', + label: 'creator', + buckets: [ + { label: 'b1', metrics: [{value: {count: 10}}] }, + { label: 'b2', metrics: [{value: {count: 1}}] } + ] + }, + { + type: 'field', + label: 'modifier', + buckets: [ + { label: 'c1', metrics: [{value: {count: 10}}] }, + { label: 'c2', metrics: [{value: {count: 1}}] } + ] + } + ]; + const data = { + list: { + context: { + facets: serverResponseFields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + expect(searchFacetFiltersService.responseFacets.length).toEqual(0); + expect(searchFacetFiltersService.tabbedFacet.fields).toEqual(['creator', 'modifier']); + expect(searchFacetFiltersService.tabbedFacet.label).toEqual('SEARCH.FILTER.PEOPLE'); + expect(searchFacetFiltersService.tabbedFacet.facets['creator'].buckets.items[0].label).toEqual('b1'); + expect(searchFacetFiltersService.tabbedFacet.facets['creator'].buckets.items[1].label).toEqual('b2'); + expect(searchFacetFiltersService.tabbedFacet.facets['modifier'].buckets.items[0].label).toEqual('c1'); + expect(searchFacetFiltersService.tabbedFacet.facets['modifier'].buckets.items[1].label).toEqual('c2'); + }); + describe('Bucket sorting', () => { let data; diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts index 56b965f0c4..eadb313632 100644 --- a/lib/content-services/src/lib/search/services/search-facet-filters.service.ts +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts @@ -27,6 +27,7 @@ import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging import { SearchFilterList } from '../models/search-filter-list.model'; import { FacetFieldBucket } from '../models/facet-field-bucket.interface'; import { CategoryService } from '../../category/services/category.service'; +import { TabbedFacetField } from '../models/tabbed-facet-field.interface'; export interface SelectedBucket { field: FacetField; @@ -45,6 +46,8 @@ export class SearchFacetFiltersService implements OnDestroy { * the newly received items are added to the responseFacets. */ responseFacets: FacetField[] = null; + /* tabbed facet incorporating creator and modifier facets */ + tabbedFacet: TabbedFacetField = null; /** shows the facet chips */ selectedBuckets: SelectedBucket[] = []; @@ -95,17 +98,18 @@ export class SearchFacetFiltersService implements OnDestroy { this.parseFacetIntervals(context); this.parseFacetQueries(context); this.sortFacets(); + this.parseTabbedFacetField(); } private parseFacetItems(context: ResultSetContext, configFacetFields: FacetField[], itemType: string) { - configFacetFields.forEach((field) => { - const responseField = this.findFacet(context, itemType, field.label); - const responseBuckets = this.getResponseBuckets(responseField, field) - .filter(this.getFilterByMinCount(field.mincount)); - this.sortFacetBuckets(responseBuckets, field.settings?.bucketSortBy, field.settings?.bucketSortDirection ?? FacetBucketSortDirection.ASCENDING); - const alreadyExistingField = this.findResponseFacet(itemType, field.label); + configFacetFields.forEach((facetField) => { + const responseField = this.findFacet(context, itemType, facetField.label); + const responseBuckets = this.getResponseBuckets(responseField, facetField) + .filter(this.getFilterByMinCount(facetField.mincount)); + this.sortFacetBuckets(responseBuckets, facetField.settings?.bucketSortBy, facetField.settings?.bucketSortDirection ?? FacetBucketSortDirection.ASCENDING); + const alreadyExistingField = this.findResponseFacet(itemType, facetField.label); - if (field.field === 'cm:categories'){ + if (facetField.field === 'cm:categories'){ this.loadCategoryNames(responseBuckets); } @@ -115,18 +119,18 @@ export class SearchFacetFiltersService implements OnDestroy { this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets); } else if (responseField) { if (responseBuckets.length > 0) { - const bucketList = new SearchFilterList(responseBuckets, field.pageSize); + const bucketList = new SearchFilterList(responseBuckets, facetField.pageSize); bucketList.filter = this.getBucketFilterFunction(bucketList); if (!this.responseFacets) { this.responseFacets = []; } this.responseFacets.push({ - ...field, + ...facetField, type: responseField.type || itemType, - label: field.label, - pageSize: field.pageSize | DEFAULT_PAGE_SIZE, - currentPageSize: field.pageSize | DEFAULT_PAGE_SIZE, + label: facetField.label, + pageSize: facetField.pageSize | DEFAULT_PAGE_SIZE, + currentPageSize: facetField.pageSize | DEFAULT_PAGE_SIZE, buckets: bucketList }); } @@ -134,6 +138,33 @@ export class SearchFacetFiltersService implements OnDestroy { }); } + private parseTabbedFacetField() { + if (this.responseFacets) { + const fields = this.responseFacets.reduce((acc, facet) => `${acc},${facet.field}`, ''); + const tabbedFacetField: TabbedFacetField = { + fields: ['creator', 'modifier'], + label: 'SEARCH.FILTER.PEOPLE', + facets: {} + }; + this.extractCreatorAndModifier(tabbedFacetField, fields); + } + } + + private extractCreatorAndModifier(tabbedFacet: TabbedFacetField, fields: string) { + if (fields.includes('creator') && fields.includes('modifier')) { + for (let i = this.responseFacets.length - 1; i >= 0; i--) { + if (this.responseFacets[i].field === 'creator' || this.responseFacets[i].field === 'modifier') { + const removedFacet = this.responseFacets.splice(i, 1)[0]; + Object.defineProperty(tabbedFacet.facets, removedFacet.field, { + value: removedFacet, + writable: true + }); + } + } + this.tabbedFacet = tabbedFacet; + } + } + private parseFacetFields(context: ResultSetContext) { const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || []; this.parseFacetItems(context, configFacetFields, 'field'); @@ -191,7 +222,6 @@ export class SearchFacetFiltersService implements OnDestroy { } } }); - } private sortFacets() { @@ -358,10 +388,10 @@ export class SearchFacetFiltersService implements OnDestroy { }); } - unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { + unselectFacetBucket(facetField: FacetField, bucket: FacetFieldBucket) { if (bucket) { bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); + this.queryBuilder.removeUserFacetBucket(facetField.field, bucket); this.updateSelectedBuckets(); this.queryBuilder.update(); } @@ -371,12 +401,14 @@ export class SearchFacetFiltersService implements OnDestroy { updateSelectedBuckets() { if (this.responseFacets) { this.selectedBuckets = []; - for (const field of this.responseFacets) { - if (field.buckets) { + let facetFields = this.tabbedFacet === null ? [] : Object.keys(this.tabbedFacet?.fields).map(field => this.tabbedFacet.facets[field]); + facetFields = [...facetFields, ...this.responseFacets]; + for (const facetField of facetFields) { + if (facetField?.buckets) { this.selectedBuckets.push( - ...this.queryBuilder.getUserFacetBuckets(field.field) + ...this.queryBuilder.getUserFacetBuckets(facetField.field) .filter((bucket) => bucket.checked) - .map((bucket) => ({field, bucket})) + .map((bucket) => ({field: facetField, bucket})) ); } } @@ -391,11 +423,11 @@ export class SearchFacetFiltersService implements OnDestroy { } resetAllSelectedBuckets() { - this.responseFacets.forEach((field) => { - if (field && field.buckets) { - for (const bucket of field.buckets.items) { + this.responseFacets.forEach((facetField) => { + if (facetField?.buckets) { + for (const bucket of facetField.buckets.items) { bucket.checked = false; - this.queryBuilder.removeUserFacetBucket(field, bucket); + this.queryBuilder.removeUserFacetBucket(facetField.field, bucket); } this.updateSelectedBuckets(); } @@ -411,6 +443,7 @@ export class SearchFacetFiltersService implements OnDestroy { reset() { this.responseFacets = []; this.selectedBuckets = []; + this.tabbedFacet = null; this.queryBuilder.resetToDefaults(); this.queryBuilder.update(); } diff --git a/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts b/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts index 88af97aa1c..2bde2193db 100644 --- a/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-query-builder.service.spec.ts @@ -637,10 +637,10 @@ describe('SearchQueryBuilder', () => { const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService); - builder.addUserFacetBucket(field1, field1buckets[0]); - builder.addUserFacetBucket(field1, field1buckets[1]); - builder.addUserFacetBucket(field2, field2buckets[0]); - builder.addUserFacetBucket(field2, field2buckets[1]); + builder.addUserFacetBucket(field1.field, field1buckets[0]); + builder.addUserFacetBucket(field1.field, field1buckets[1]); + builder.addUserFacetBucket(field2.field, field2buckets[0]); + builder.addUserFacetBucket(field2.field, field2buckets[1]); const compiledQuery = builder.buildQuery(); const expectedResult = '(f1-q1 OR f1-q2) AND (f2-q1 OR f2-q2)';