diff --git a/docs/content-services/components/search-chip-input.component.md b/docs/content-services/components/search-chip-input.component.md deleted file mode 100644 index ea38705a60..0000000000 --- a/docs/content-services/components/search-chip-input.component.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -Title: Search Chip Input component -Added: v6.1.0 -Status: Active -Last reviewed: 2023-06-01 ---- - -# [Search Chip Input component](../../../lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts "Defined in search-chip-input.component.ts") - -Represents an input with stacked list of chips as phrases added through input. - -![Search Chip Input](../../docassets/images/search-chip-input.png) - -## Basic usage - -```html - - -``` - -### Properties - -| Name | Type | Default value | Description | -| ---- | ---- | ------------- | ----------- | -| label | `string` | | Label that will be associated with the input | -| addOnBlur | `boolean` | true | Specifies whether new phrase will be added when input blurs | -| onReset | [`Observable`](https://rxjs.dev/guide/observable)`` | | Observable that will listen to any reset event causing component to clear the chips and input | - -### Events - -| Name | Type | Description | -| ---- | ---- | ----------- | -| phrasesChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when new phrase is entered | - -## 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 Logical Filter component](search-logical-filter.component.md) -- [Search check list component](search-check-list.component.md) -- [Search date range component](search-date-range.component.md) -- [Search number range component](search-number-range.component.md) -- [Search radio component](search-radio.component.md) -- [Search slider component](search-slider.component.md) -- [Search text component](search-text.component.md) diff --git a/docs/content-services/components/search-logical-filter.component.md b/docs/content-services/components/search-logical-filter.component.md index 30d7bb04c7..a5c79ae7da 100644 --- a/docs/content-services/components/search-logical-filter.component.md +++ b/docs/content-services/components/search-logical-filter.component.md @@ -7,7 +7,7 @@ Last reviewed: 2023-06-01 # [Search Logical Filter component](../../../lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts "Defined in search-logical-filter.component.ts") -Implements a [search widget](../../../lib/content-services/src/lib/search/models/search-widget.interface.ts) consisting of 3 chip inputs representing logical conditions to form search query from. +Implements a [search widget](../../../lib/content-services/src/lib/search/models/search-widget.interface.ts) consisting of 4 inputs representing logical conditions to form search query from. ![Search Logical Filter](../../docassets/images/search-logical-filter.png) @@ -45,14 +45,12 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models ## Details This component lets the user provide logical conditions to apply to each `field` in the search query. -See the [Search chip input component](search-chip-input.component.md) for full details of how to use chip inputs. ## 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 Input component](search-chip-input.component.md) - [Search check list component](search-check-list.component.md) - [Search date range component](search-date-range.component.md) - [Search number range component](search-number-range.component.md) diff --git a/docs/docassets/images/search-logical-filter.png b/docs/docassets/images/search-logical-filter.png index b9c826aaac..b78f81cf30 100644 Binary files a/docs/docassets/images/search-logical-filter.png and b/docs/docassets/images/search-logical-filter.png differ diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 59caf12cf0..511c1f9032 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -401,15 +401,18 @@ } }, "LOGICAL_SEARCH": { - "SEARCH_CHIP_INPUT": { - "ADD_PHRASE": "Add phrase..." - }, "MATCH_ALL": "ALL", - "MATCH_ALL_LABEL": "Match ALL of these phrases", + "MATCH_ALL_LABEL": "Match ALL of these words", + "MATCH_ALL_HINT": "Results will match all words entered here", "MATCH_ANY": "ANY", - "MATCH_ANY_LABEL": "Match ANY of these phrases", + "MATCH_ANY_LABEL": "Match ANY of these words", + "MATCH_ANY_HINT": "Results will match any words entered here", + "MATCH_EXACT": "EXACT", + "MATCH_EXACT_LABEL": "Match this EXACT PHRASE", + "MATCH_EXACT_HINT": "Results will match this entire phrase", "EXCLUDE": "EXCLUDE", - "EXCLUDE_LABEL": "EXCLUDE these phrases" + "EXCLUDE_LABEL": "EXCLUDE these words", + "EXCLUDE_HINT": "Results will exclude matches with these words" } }, "PERMISSION": { diff --git a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.html b/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.html deleted file mode 100644 index be72cd993b..0000000000 --- a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.html +++ /dev/null @@ -1,15 +0,0 @@ -{{label | translate}} - - - {{phrase}} - - - - diff --git a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.scss b/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.scss deleted file mode 100644 index 8733000835..0000000000 --- a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.scss +++ /dev/null @@ -1,18 +0,0 @@ -.adf-search-chip-input { - padding-bottom: 15px; - - .mat-chip-list-wrapper { - border: 1px solid var(--adf-theme-mat-grey-color-a400); - border-radius: 5px; - margin-top: 5px; - - .mat-chip { - word-break: break-all; - height: unset; - } - - input { - height: 25px; - } - } -} diff --git a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.spec.ts b/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.spec.ts deleted file mode 100644 index 0e3e85d279..0000000000 --- a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -/*! - * @license - * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatChip, MatChipRemove } from '@angular/material/chips'; -import { By } from '@angular/platform-browser'; -import { TranslateModule } from '@ngx-translate/core'; -import { Subject } from 'rxjs'; -import { ContentTestingModule } from '../../../testing/content.testing.module'; -import { SearchChipInputComponent } from './search-chip-input.component'; - -describe('SearchChipInputComponent', () => { - let component: SearchChipInputComponent; - let fixture: ComponentFixture; - const onResetSubject = new Subject(); - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [SearchChipInputComponent], - imports: [ - TranslateModule.forRoot(), - ContentTestingModule - ] - }); - - fixture = TestBed.createComponent(SearchChipInputComponent); - component = fixture.componentInstance; - component.onReset = onResetSubject.asObservable(); - fixture.detectChanges(); - }); - - afterEach(() => removeAllChips()); - - function getChipInput(): HTMLInputElement { - return fixture.debugElement.query(By.css('input')).nativeElement; - } - - function getChipList(): MatChip[] { - return fixture.debugElement.queryAll(By.css('mat-chip')).map((chip) => chip.nativeElement); - } - - function getChipValue(index: number): string { - return fixture.debugElement.queryAll(By.css('mat-chip span')).map((chip) => chip.nativeElement)[index].innerText; - } - - function enterNewChip(value: string) { - const input = getChipInput(); - input.value = value; - fixture.detectChanges(); - input.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); - fixture.detectChanges(); - } - - function removeAllChips() { - const chips = getChipList(); - if (!!chips && chips.length > 0) { - chips.forEach((chip) => chip.remove()); - } - } - - function removeChip(index: number) { - const removeBtns = fixture.debugElement.queryAll(By.directive(MatChipRemove)).map((removeBtn) => removeBtn.nativeElement); - removeBtns[index].click(); - fixture.detectChanges(); - } - - it('should display label provided as component input', () => { - const label = 'Test'; - component.label = label; - fixture.detectChanges(); - const matLabel = fixture.debugElement.query(By.css('mat-label')).nativeElement.innerText; - expect(matLabel).toBe(label); - }); - - it('should display proper placeholder for chip input', () => { - const input = getChipInput(); - expect(input.placeholder).toBe('SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE'); - }); - - it('should not display any chips initially', () => { - const chips = getChipList(); - expect(chips).toEqual([]); - }); - - it('should add new chip when input has value and enter was hit', () => { - const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit'); - enterNewChip('test'); - expect(phrasesChangedSpy).toHaveBeenCalledOnceWith(['test']); - expect(getChipList().length).toBe(1); - expect(getChipValue(0)).toBe('test'); - }); - - it('should add input value as whole phrase even if it contains whitespaces and special signs', () => { - const phrase = 'test another world &*,.;""!@#$$%^*()[]-+='; - enterNewChip(phrase); - expect(getChipList().length).toBe(1); - expect(getChipValue(0)).toBe(phrase); - }); - - it('should add new chip when input is blurred', () => { - const input = getChipInput(); - input.value = 'test'; - fixture.detectChanges(); - input.dispatchEvent(new InputEvent('blur')); - fixture.detectChanges(); - expect(input.value).toBe(''); - expect(getChipList().length).toBe(1); - expect(getChipValue(0)).toBe('test'); - }); - - it('should not add new chip when input is blurred if addOnBlur is false', () => { - component.addOnBlur = false; - const input = getChipInput(); - input.value = 'test'; - fixture.detectChanges(); - input.dispatchEvent(new InputEvent('blur')); - fixture.detectChanges(); - expect(input.value).toBe('test'); - expect(getChipList().length).toBe(0); - }); - - it('should clear the input after new chip is added', () => { - const input = getChipInput(); - enterNewChip('test2'); - expect(input.value).toBe(''); - }); - - it('should reset all chips when onReset event is emitted', () => { - enterNewChip('test1'); - enterNewChip('test2'); - enterNewChip('test3'); - const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit'); - onResetSubject.next(); - fixture.detectChanges(); - expect(phrasesChangedSpy).toHaveBeenCalledOnceWith([]); - expect(getChipList()).toEqual([]); - }); - - it('should remove chip upon clicking remove button', () => { - enterNewChip('test1'); - enterNewChip('test2'); - const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit'); - removeChip(0); - expect(phrasesChangedSpy).toHaveBeenCalledOnceWith(['test2']); - expect(getChipList().length).toEqual(1); - }); -}); diff --git a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts b/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts deleted file mode 100644 index d85f562b45..0000000000 --- a/lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*! - * @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 { ENTER } from '@angular/cdk/keycodes'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { Observable, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -@Component({ - selector: 'adf-search-chip-input', - templateUrl: './search-chip-input.component.html', - styleUrls: ['./search-chip-input.component.scss'], - encapsulation: ViewEncapsulation.None, - host: { class: 'adf-search-chip-input' } -}) -export class SearchChipInputComponent implements OnInit, OnDestroy { - @Input() - label: string; - - @Input() - addOnBlur = true; - - @Input() - onReset: Observable; - - @Output() - phrasesChanged: EventEmitter = new EventEmitter(); - - private onDestroy$ = new Subject(); - phrases: string[] = []; - readonly separatorKeysCodes = [ENTER] as const; - - ngOnInit() { - this.onReset?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.resetChips()); - } - - ngOnDestroy(): void { - this.onDestroy$.next(); - this.onDestroy$.complete(); - } - - addPhrase(event: MatChipInputEvent) { - const phrase = (event.value || '').trim(); - - if (phrase) { - this.phrases.push(phrase); - this.phrasesChanged.emit(this.phrases); - } - event.chipInput.clear(); - } - - removePhrase(index: number) { - this.phrases.splice(index, 1); - this.phrasesChanged.emit(this.phrases); - } - - private resetChips() { - this.phrases = []; - this.phrasesChanged.emit(this.phrases); - } -} diff --git a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.html b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.html index 05606fad33..07d32d2ea1 100644 --- a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.html +++ b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.html @@ -1,17 +1,10 @@
- - - - - - +
+ {{('SEARCH.LOGICAL_SEARCH.' + field + '_LABEL') | translate}} + +
diff --git a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.scss b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.scss index 99943c9939..a467b7e956 100644 --- a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.scss +++ b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.scss @@ -1,4 +1,21 @@ .adf-search-logical-filter-container { - display: flex; - flex-direction: column; + .adf-search-input { + display: flex; + flex-direction: column; + padding-bottom: 15px; + + &:last-child { + padding: 0; + } + + input { + height: 25px; + border: 1px solid var(--adf-theme-mat-grey-color-a400); + border-radius: 5px; + margin-top: 5px; + padding: 5px; + font-size: 14px; + color: var(--adf-theme-foreground-text-color); + } + } } diff --git a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.spec.ts b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.spec.ts index dc08d69c5a..27077aaaaa 100644 --- a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.spec.ts @@ -19,7 +19,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; import { ContentTestingModule } from '../../../testing/content.testing.module'; -import { SearchChipInputComponent } from '../search-chip-input/search-chip-input.component'; import { LogicalSearchCondition, LogicalSearchFields, SearchLogicalFilterComponent } from './search-logical-filter.component'; describe('SearchLogicalFilterComponent', () => { @@ -28,7 +27,7 @@ describe('SearchLogicalFilterComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [SearchLogicalFilterComponent, SearchChipInputComponent], + declarations: [SearchLogicalFilterComponent], imports: [ TranslateModule.forRoot(), ContentTestingModule @@ -48,19 +47,19 @@ describe('SearchLogicalFilterComponent', () => { fixture.detectChanges(); }); - function getChipInputs(): HTMLInputElement[] { - return fixture.debugElement.queryAll(By.css('adf-search-chip-input input')).map((input) => input.nativeElement); + function getInputs(): HTMLInputElement[] { + return fixture.debugElement.queryAll(By.css('.adf-search-input input')).map((input) => input.nativeElement); } - function getChipInputsLabels(): string[] { - return fixture.debugElement.queryAll(By.css('adf-search-chip-input mat-label')).map((label) => label.nativeElement.innerText); + function getInputsLabels(): string[] { + return fixture.debugElement.queryAll(By.css('.adf-search-input mat-label')).map((label) => label.nativeElement.innerText); } function enterNewPhrase(value: string, index: number) { - const inputs = getChipInputs(); + const inputs = getInputs(); inputs[index].value = value; - fixture.detectChanges(); - inputs[index].dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); + inputs[index].dispatchEvent(new Event('input')); + inputs[index].dispatchEvent(new Event('change')); fixture.detectChanges(); } @@ -74,12 +73,13 @@ describe('SearchLogicalFilterComponent', () => { expect(component.hasValidValue()).toBeFalse(); }); - it('should contain 3 chip input components with correct labels', () => { - const labels = getChipInputsLabels(); - expect(labels.length).toBe(3); + it('should contain 4 inputs with correct labels', () => { + const labels = getInputsLabels(); + expect(labels.length).toBe(4); expect(labels[0]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ALL_LABEL'); expect(labels[1]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ANY_LABEL'); expect(labels[2]).toBe('SEARCH.LOGICAL_SEARCH.EXCLUDE_LABEL'); + expect(labels[3]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_EXACT_LABEL'); }); it('should has valid value after phrase is entered', () => { @@ -88,36 +88,34 @@ describe('SearchLogicalFilterComponent', () => { }); it('should update display value after phrases changes', () => { - spyOn(component, 'onPhraseChange').and.callThrough(); spyOn(component.displayValue$, 'next'); enterNewPhrase('test2', 0); - expect(component.onPhraseChange).toHaveBeenCalled(); - expect(component.displayValue$.next).toHaveBeenCalledOnceWith(` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test2`); + expect(component.displayValue$.next).toHaveBeenCalledOnceWith(` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test2`); }); it('should have correct display value after each field has at least one phrase', () => { - spyOn(component, 'onPhraseChange').and.callThrough(); spyOn(component.displayValue$, 'next'); enterNewPhrase('test1', 0); enterNewPhrase('test2', 1); enterNewPhrase('test3', 2); - const displayVal1 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test1`; - const displayVal2 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[1]}: test2`; - const displayVal3 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[2]}: test3`; - expect(component.onPhraseChange).toHaveBeenCalled(); - expect(component.displayValue$.next).toHaveBeenCalledWith(displayVal1 + displayVal2 + displayVal3); + enterNewPhrase('test4', 3); + const displayVal1 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test1`; + const displayVal2 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[1]}: test2`; + const displayVal3 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[2]}: test3`; + const displayVal4 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[3]}: test4`; + expect(component.displayValue$.next).toHaveBeenCalledWith(displayVal1 + displayVal2 + displayVal4 + displayVal3); }); it('should set correct value and update display value', () => { spyOn(component.displayValue$, 'next'); - const searchCondition: LogicalSearchCondition = { matchAll: ['test1'], matchAny: ['test2'], exclude: ['test3'] }; + const searchCondition: LogicalSearchCondition = { matchAll: 'test1', matchAny: 'test2', exclude: 'test3', matchExact: 'test4' }; component.setValue(searchCondition); expect(component.getCurrentValue()).toEqual(searchCondition); expect(component.displayValue$.next).toHaveBeenCalled(); }); it('should reset value and display value when reset is called', () => { - const searchCondition: LogicalSearchCondition = { matchAll: ['test1'], matchAny: ['test2'], exclude: ['test3'] }; + const searchCondition: LogicalSearchCondition = { matchAll: 'test1', matchAny: 'test2', exclude: 'test3', matchExact: 'test4' }; component.setValue(searchCondition); fixture.detectChanges(); spyOn(component.context, 'update'); @@ -125,14 +123,13 @@ describe('SearchLogicalFilterComponent', () => { component.reset(); expect(component.context.queryFragments[component.id]).toBe(''); expect(component.context.update).toHaveBeenCalled(); - expect(component.getCurrentValue()).toEqual({ matchAll: [], matchAny: [], exclude: [] }); + expect(component.getCurrentValue()).toEqual({ matchAll: '', matchAny: '', exclude: '', matchExact: '' }); expect(component.displayValue$.next).toHaveBeenCalledWith(''); }); it('should form correct query from match all field', () => { spyOn(component.context, 'update'); - enterNewPhrase('test1', 0); - enterNewPhrase('test2', 0); + enterNewPhrase(' test1 test2 ', 0); component.submitValues(); expect(component.context.update).toHaveBeenCalled(); expect(component.context.queryFragments[component.id]).toBe('((field1:"test1" AND field1:"test2") OR (field2:"test1" AND field2:"test2"))'); @@ -140,8 +137,7 @@ describe('SearchLogicalFilterComponent', () => { it('should form correct query from match any field', () => { spyOn(component.context, 'update'); - enterNewPhrase('test3', 1); - enterNewPhrase('test4', 1); + enterNewPhrase(' test3 test4', 1); component.submitValues(); expect(component.context.update).toHaveBeenCalled(); expect(component.context.queryFragments[component.id]).toBe('((field1:"test3" OR field1:"test4") OR (field2:"test3" OR field2:"test4"))'); @@ -149,23 +145,32 @@ describe('SearchLogicalFilterComponent', () => { it('should form correct query from exclude field', () => { spyOn(component.context, 'update'); - enterNewPhrase('test5', 2); - enterNewPhrase('test6', 2); + enterNewPhrase('test5 test6 ', 2); component.submitValues(); expect(component.context.update).toHaveBeenCalled(); expect(component.context.queryFragments[component.id]).toBe('((NOT field1:"test5" AND NOT field1:"test6") AND (NOT field2:"test5" AND NOT field2:"test6"))'); }); + it('should form correct query from match exact field and trim it', () => { + spyOn(component.context, 'update'); + enterNewPhrase(' test7 test8 ', 3); + component.submitValues(); + expect(component.context.update).toHaveBeenCalled(); + expect(component.context.queryFragments[component.id]).toBe('((field1:"test7 test8") OR (field2:"test7 test8"))'); + }); + it('should form correct joined query from all fields', () => { spyOn(component.context, 'update'); enterNewPhrase('test1', 0); enterNewPhrase('test2', 1); enterNewPhrase('test3', 2); + enterNewPhrase('test4', 3); component.submitValues(); const subQuery1 = '((field1:"test1") OR (field2:"test1"))'; const subQuery2 = '((field1:"test2") OR (field2:"test2"))'; const subQuery3 = '((NOT field1:"test3") AND (NOT field2:"test3"))'; + const subQuery4 = '((field1:"test4") OR (field2:"test4"))'; expect(component.context.update).toHaveBeenCalled(); - expect(component.context.queryFragments[component.id]).toBe(`${subQuery1} AND ${subQuery2} AND ${subQuery3}`); + expect(component.context.queryFragments[component.id]).toBe(`${subQuery1} AND ${subQuery2} AND ${subQuery4} AND ${subQuery3}`); }); }); diff --git a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts index 6ae5784ff9..5bbeed7703 100644 --- a/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts +++ b/lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts @@ -25,10 +25,11 @@ import { TranslationService } from '@alfresco/adf-core'; export enum LogicalSearchFields { MATCH_ALL = 'matchAll', MATCH_ANY = 'matchAny', - EXCLUDE = 'exclude' + EXCLUDE = 'exclude', + MATCH_EXACT = 'matchExact' } -export type LogicalSearchConditionEnumValuedKeys = { [T in LogicalSearchFields]: string[]; }; +export type LogicalSearchConditionEnumValuedKeys = { [T in LogicalSearchFields]: string; }; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface LogicalSearchCondition extends LogicalSearchConditionEnumValuedKeys {} @@ -39,26 +40,22 @@ export interface LogicalSearchCondition extends LogicalSearchConditionEnumValued encapsulation: ViewEncapsulation.None }) export class SearchLogicalFilterComponent implements SearchWidget, OnInit { - private searchCondition: LogicalSearchCondition; - private reset$ = new Subject(); - id: string; settings?: SearchWidgetSettings; context?: SearchQueryBuilderService; startValue: string; - displayValue$: Subject = new Subject(); - resetObservable = this.reset$.asObservable(); + searchCondition: LogicalSearchCondition; + fields = Object.keys(LogicalSearchFields); LogicalSearchFields = LogicalSearchFields; + displayValue$: Subject = new Subject(); constructor(private translationService: TranslationService) {} ngOnInit(): void { - this.searchCondition = { matchAll: [], matchAny: [], exclude: [] }; - this.updateDisplayValue(); + this.clearSearchInputs(); } - onPhraseChange(phrases: string[], field: LogicalSearchFields) { - this.searchCondition[field] = phrases; + onInputChange() { this.updateDisplayValue(); } @@ -68,11 +65,12 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit { const fields = this.settings.field.split(',').map((field) => field += ':'); let query = ''; Object.keys(this.searchCondition).forEach((key) => { - if (this.searchCondition[key].length > 0) { + if (this.searchCondition[key] !== '') { let connector = ''; let subQuery = ''; switch(key) { case LogicalSearchFields.MATCH_ALL: + case LogicalSearchFields.MATCH_EXACT: connector = 'AND'; break; case LogicalSearchFields.MATCH_ANY: @@ -87,12 +85,16 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit { fields.forEach((field) => { subQuery += subQuery === '' ? '' : key === LogicalSearchFields.EXCLUDE ? ' AND ' : ' OR '; let fieldQuery = '('; - this.searchCondition[key].forEach((phrase: string) => { - const refinedPhrase = '\"' + phrase + '\"'; - fieldQuery += fieldQuery === '(' ? - `${key === LogicalSearchFields.EXCLUDE ? 'NOT ' : ''}${field}${refinedPhrase}` : - ` ${connector} ${field}${refinedPhrase}`; - }); + if (key === LogicalSearchFields.MATCH_EXACT) { + fieldQuery += field + '"' + this.searchCondition[key].trim() + '"'; + } else { + this.searchCondition[key].split(' ').filter((condition: string) => condition !== '').forEach((phrase: string) => { + const refinedPhrase = '\"' + phrase + '\"'; + fieldQuery += fieldQuery === '(' ? + `${key === LogicalSearchFields.EXCLUDE ? 'NOT ' : ''}${field}${refinedPhrase}` : + ` ${connector} ${field}${refinedPhrase}`; + }); + } subQuery += `${fieldQuery})`; }); query += query === '' ? `(${subQuery})` : ` AND (${subQuery})`; @@ -105,7 +107,7 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit { } hasValidValue(): boolean { - return Object.keys(this.searchCondition).some((key: string) => this.searchCondition[key].length !== 0); + return Object.keys(this.searchCondition).some((key: string) => this.searchCondition[key] !== ''); } getCurrentValue(): LogicalSearchCondition { @@ -119,9 +121,8 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit { reset() { if (this.id && this.context) { - this.reset$.next(); this.context.queryFragments[this.id] = ''; - this.updateDisplayValue(); + this.clearSearchInputs(); this.context.update(); } } @@ -130,13 +131,17 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit { if (this.hasValidValue()) { const displayValue = Object.keys(this.searchCondition).reduce((acc, key) => { const fieldIndex = Object.values(LogicalSearchFields).indexOf(key as LogicalSearchFields); - const fieldKeyTranslated = this.translationService.instant(`SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[fieldIndex]}`); - const stackedPhrases = this.searchCondition[key].reduce((phraseAcc, phrase) => `${phraseAcc === '' ? phraseAcc : phraseAcc + ','} ${phrase}`, ''); - return stackedPhrases !== '' ? `${acc} ${fieldKeyTranslated}: ${stackedPhrases}` : acc; + const fieldKeyTranslated = this.translationService.instant(`SEARCH.LOGICAL_SEARCH.${this.fields[fieldIndex]}`); + return this.searchCondition[key] !== '' ? `${acc} ${fieldKeyTranslated}: ${this.searchCondition[key]}` : acc; }, ''); this.displayValue$.next(displayValue); } else { this.displayValue$.next(''); } } + + private clearSearchInputs(): void { + this.searchCondition = { matchAll: '', matchAny: '', matchExact: '', exclude: '' }; + this.updateDisplayValue(); + } } diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts index 9fa4f9e752..d47030feac 100644 --- a/lib/content-services/src/lib/search/public-api.ts +++ b/lib/content-services/src/lib/search/public-api.ts @@ -60,7 +60,6 @@ export * from './components/search-form/search-form.component'; export * from './components/search-filter-chips/search-filter-chips.component'; export * from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component'; export * from './components/search-facet-field/search-facet-field.component'; -export * from './components/search-chip-input/search-chip-input.component'; export * from './components/search-logical-filter/search-logical-filter.component'; export * from './components/reset-search.directive'; export * from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component'; diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index b28271cb9d..a52b017b58 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -49,7 +49,6 @@ import { SearchFilterMenuCardComponent } from './components/search-filter-chips/ import { SearchFacetFieldComponent } from './components/search-facet-field/search-facet-field.component'; import { SearchWidgetChipComponent } from './components/search-filter-chips/search-widget-chip/search-widget-chip.component'; import { SearchFacetChipComponent } from './components/search-filter-chips/search-facet-chip/search-facet-chip.component'; -import { SearchChipInputComponent } from './components/search-chip-input/search-chip-input.component'; import { SearchLogicalFilterComponent } from './components/search-logical-filter/search-logical-filter.component'; import { ResetSearchDirective } from './components/reset-search.directive'; @@ -88,7 +87,6 @@ import { ResetSearchDirective } from './components/reset-search.directive'; SearchFacetFieldComponent, SearchWidgetChipComponent, SearchFacetChipComponent, - SearchChipInputComponent, SearchLogicalFilterComponent, ResetSearchDirective ], @@ -115,7 +113,6 @@ import { ResetSearchDirective } from './components/reset-search.directive'; SearchFilterChipsComponent, SearchFilterMenuCardComponent, SearchFacetFieldComponent, - SearchChipInputComponent, SearchLogicalFilterComponent, ResetSearchDirective ],