From f8f72d7f1a88b0c8af464715fa23f10a4ecb5889 Mon Sep 17 00:00:00 2001 From: MichalKinas <113341662+MichalKinas@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:06:05 +0200 Subject: [PATCH] [ACS-5179] Add search facet tabbed component for creator and modifier (#8775) --- .../components/search-facet-chip-tabbed.md | 49 ++++ .../images/search-facet-chip-tabbed.png | Bin 0 -> 41202 bytes docs/versionIndex.md | 1 + lib/content-services/src/lib/i18n/en.json | 7 +- ...earch-chip-autocomplete-input.component.ts | 2 +- .../search-facet-field.component.spec.ts | 24 +- .../search-facet-field.component.ts | 6 +- .../search-facet-chip-tabbed.component.html | 59 +++++ .../search-facet-chip-tabbed.component.scss | 17 ++ ...search-facet-chip-tabbed.component.spec.ts | 232 ++++++++++++++++++ .../search-facet-chip-tabbed.component.ts | 174 +++++++++++++ .../search-filter-chips.component.html | 6 + .../search-filter-chips.component.scss | 3 +- .../search-filter-chips.component.spec.ts | 6 +- .../search-filter-chips.component.ts | 16 ++ .../search-filter.component.spec.ts | 6 +- .../models/tabbed-facet-field.interface.ts | 29 +++ .../src/lib/search/public-api.ts | 2 + .../src/lib/search/search.module.ts | 7 +- .../services/base-query-builder.service.ts | 16 +- .../search-facet-filters.service.spec.ts | 51 +++- .../services/search-facet-filters.service.ts | 79 ++++-- .../search-query-builder.service.spec.ts | 8 +- 23 files changed, 738 insertions(+), 62 deletions(-) create mode 100644 docs/content-services/components/search-facet-chip-tabbed.md create mode 100644 docs/docassets/images/search-facet-chip-tabbed.png create mode 100644 lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.html create mode 100644 lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.scss create mode 100644 lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.spec.ts create mode 100644 lib/content-services/src/lib/search/components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component.ts create mode 100644 lib/content-services/src/lib/search/models/tabbed-facet-field.interface.ts 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 0000000000000000000000000000000000000000..d48babab5dca996b5580452c4297dc7909053724 GIT binary patch literal 41202 zcmeEuWn7e9*DnkW5-Ny*gkm5_mmn!6AV@buNym^ww+ey?DoBTP58Wvx-Q68S!_Wig zLh$xJ&+mP|oG<6g13#FHeeGC#@3q!m>%Z3KqnxY+J}x;f3JMCo@*!Y^(FI zGxp-#Zb{I%tAi2EYs}{JtI)tqw2&vyh+5E3`@elvTuDyz!nQ{t8$r<~;MQsU6o8AB zcca*oc`Fx9bPSvyZiXzpsJ(+vB^Zx~atp(TmpMKfYeyJm+bs8$77B`dTeJ)#ktA!j zkn|0-cTQi3joWOBiPPG5z>zXqH}K}5D4|Nv@7X*?$qc7?!@OYh5`WOcpYlDQHxUXa zZ=-;-^EYRrG+_&TOHm?e1lg+K%G+}pc6YUJ4XyW@cgueTD0tE6gtkYXd#28NhF58| zh7d}tDHk-}XYJ!@sG97Ey!W_?p6C##pZ3B1p?62VUQY`B?xwtzd^xvxkdDe7k70+Y zN}lkUPgdw|BPJ0Rjveh!5;vC*H2(Ok6Zcm*gi&7*=xh6v?(Cr%kZ?seelsAh>G^Pj zH85kQ+qRLdRzc_R5zo%+_vi3u0-8U9Gwv8RJ$m`td!$M(ga|dCzLDUVCtnXYI$umZ z+y6dA{zGx{TAoH$H@&=CEoz1iBCIC5b?LobzokISwp)3$#7>^VDq8-0f@&|qX#=Ud zFe)2K+2;N1kB%j0MeJsG&_IiJEpa>Vck*##2@^pVcaPO#9<&81jLfH=P3S&_;O0wv z=w<6ai<3j)PS&FCCL$4SYMpNkEa3Sezw5NUNOLbw2qggxCwoG+I=34Y?0z3=R5)`i7

f_D0ql@3(fz2 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 literal 0 HcmV?d00001 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)';