[ACS-5436] Logical search final version (#8709)

* [ACS-5436] Logical search final version

* [ACS-5436] CR fixes

* [ACS-5436] Remove unnecessary escape characters
This commit is contained in:
MichalKinas
2023-06-28 06:55:47 +02:00
committed by GitHub
parent 7abebf0652
commit ee588df85b
14 changed files with 102 additions and 404 deletions

View File

@@ -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
<adf-search-chip-input
[label]="'Some label'"
[onReset]="onResetObservable"
(phrasesChanged)="handlePhraseChanged($event)">
</adf-search-chip-input>
```
### 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)`<void>` | | 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)`<string[]>` | 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)

View File

@@ -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") # [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) ![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 ## Details
This component lets the user provide logical conditions to apply to each `field` in the search query. 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 ## See also
- [Search Configuration Guide](../../user-guide/search-configuration-guide.md) - [Search Configuration Guide](../../user-guide/search-configuration-guide.md)
- [Search Query Builder service](../services/search-query-builder.service.md) - [Search Query Builder service](../services/search-query-builder.service.md)
- [Search Widget Interface](../interfaces/search-widget.interface.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 check list component](search-check-list.component.md)
- [Search date range component](search-date-range.component.md) - [Search date range component](search-date-range.component.md)
- [Search number range component](search-number-range.component.md) - [Search number range component](search-number-range.component.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -401,15 +401,18 @@
} }
}, },
"LOGICAL_SEARCH": { "LOGICAL_SEARCH": {
"SEARCH_CHIP_INPUT": {
"ADD_PHRASE": "Add phrase..."
},
"MATCH_ALL": "ALL", "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": "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": "EXCLUDE",
"EXCLUDE_LABEL": "EXCLUDE these phrases" "EXCLUDE_LABEL": "EXCLUDE these words",
"EXCLUDE_HINT": "Results will exclude matches with these words"
} }
}, },
"PERMISSION": { "PERMISSION": {

View File

@@ -1,15 +0,0 @@
<mat-label>{{label | translate}}</mat-label>
<mat-chip-list #chipList [attr.aria-label]="label | translate">
<mat-chip *ngFor="let phrase of phrases; let i = index" (removed)="removePhrase(i)">
<span>{{phrase}}</span>
<button matChipRemove [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + phrase">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
<input placeholder="{{ 'SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE' | translate }}"
[attr.aria-label]="'SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE' | translate"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="addPhrase($event)" />
</mat-chip-list>

View File

@@ -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;
}
}
}

View File

@@ -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<SearchChipInputComponent>;
const onResetSubject = new Subject<void>();
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);
});
});

View File

@@ -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<void>;
@Output()
phrasesChanged: EventEmitter<string[]> = new EventEmitter();
private onDestroy$ = new Subject<void>();
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);
}
}

View File

@@ -1,17 +1,10 @@
<div class="adf-search-logical-filter-container"> <div class="adf-search-logical-filter-container">
<adf-search-chip-input <div *ngFor="let field of fields" class="adf-search-input">
[label]="'SEARCH.LOGICAL_SEARCH.MATCH_ALL_LABEL' | translate" <mat-label>{{('SEARCH.LOGICAL_SEARCH.' + field + '_LABEL') | translate}}</mat-label>
[onReset]="resetObservable" <input type="text"
(phrasesChanged)="onPhraseChange($event, LogicalSearchFields.MATCH_ALL)"> [(ngModel)]="searchCondition[LogicalSearchFields[field]]"
</adf-search-chip-input> placeholder="{{ ('SEARCH.LOGICAL_SEARCH.' + field + '_HINT') | translate }}"
<adf-search-chip-input [attr.aria-label]="('SEARCH.LOGICAL_SEARCH.' + field + '_HINT') | translate"
[label]="'SEARCH.LOGICAL_SEARCH.MATCH_ANY_LABEL' | translate" (change)="onInputChange()"/>
[onReset]="resetObservable" </div>
(phrasesChanged)="onPhraseChange($event, LogicalSearchFields.MATCH_ANY)">
</adf-search-chip-input>
<adf-search-chip-input
[label]="'SEARCH.LOGICAL_SEARCH.EXCLUDE_LABEL' | translate"
[onReset]="resetObservable"
(phrasesChanged)="onPhraseChange($event, LogicalSearchFields.EXCLUDE)">
</adf-search-chip-input>
</div> </div>

View File

@@ -1,4 +1,21 @@
.adf-search-logical-filter-container { .adf-search-logical-filter-container {
display: flex; .adf-search-input {
flex-direction: column; 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);
}
}
} }

View File

@@ -19,7 +19,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ContentTestingModule } from '../../../testing/content.testing.module'; 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'; import { LogicalSearchCondition, LogicalSearchFields, SearchLogicalFilterComponent } from './search-logical-filter.component';
describe('SearchLogicalFilterComponent', () => { describe('SearchLogicalFilterComponent', () => {
@@ -28,7 +27,7 @@ describe('SearchLogicalFilterComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [SearchLogicalFilterComponent, SearchChipInputComponent], declarations: [SearchLogicalFilterComponent],
imports: [ imports: [
TranslateModule.forRoot(), TranslateModule.forRoot(),
ContentTestingModule ContentTestingModule
@@ -48,19 +47,19 @@ describe('SearchLogicalFilterComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
function getChipInputs(): HTMLInputElement[] { function getInputs(): HTMLInputElement[] {
return fixture.debugElement.queryAll(By.css('adf-search-chip-input input')).map((input) => input.nativeElement); return fixture.debugElement.queryAll(By.css('.adf-search-input input')).map((input) => input.nativeElement);
} }
function getChipInputsLabels(): string[] { function getInputsLabels(): string[] {
return fixture.debugElement.queryAll(By.css('adf-search-chip-input mat-label')).map((label) => label.nativeElement.innerText); return fixture.debugElement.queryAll(By.css('.adf-search-input mat-label')).map((label) => label.nativeElement.innerText);
} }
function enterNewPhrase(value: string, index: number) { function enterNewPhrase(value: string, index: number) {
const inputs = getChipInputs(); const inputs = getInputs();
inputs[index].value = value; inputs[index].value = value;
fixture.detectChanges(); inputs[index].dispatchEvent(new Event('input'));
inputs[index].dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); inputs[index].dispatchEvent(new Event('change'));
fixture.detectChanges(); fixture.detectChanges();
} }
@@ -74,12 +73,13 @@ describe('SearchLogicalFilterComponent', () => {
expect(component.hasValidValue()).toBeFalse(); expect(component.hasValidValue()).toBeFalse();
}); });
it('should contain 3 chip input components with correct labels', () => { it('should contain 4 inputs with correct labels', () => {
const labels = getChipInputsLabels(); const labels = getInputsLabels();
expect(labels.length).toBe(3); expect(labels.length).toBe(4);
expect(labels[0]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ALL_LABEL'); expect(labels[0]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ALL_LABEL');
expect(labels[1]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ANY_LABEL'); expect(labels[1]).toBe('SEARCH.LOGICAL_SEARCH.MATCH_ANY_LABEL');
expect(labels[2]).toBe('SEARCH.LOGICAL_SEARCH.EXCLUDE_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', () => { it('should has valid value after phrase is entered', () => {
@@ -88,36 +88,34 @@ describe('SearchLogicalFilterComponent', () => {
}); });
it('should update display value after phrases changes', () => { it('should update display value after phrases changes', () => {
spyOn(component, 'onPhraseChange').and.callThrough();
spyOn(component.displayValue$, 'next'); spyOn(component.displayValue$, 'next');
enterNewPhrase('test2', 0); 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', () => { it('should have correct display value after each field has at least one phrase', () => {
spyOn(component, 'onPhraseChange').and.callThrough();
spyOn(component.displayValue$, 'next'); spyOn(component.displayValue$, 'next');
enterNewPhrase('test1', 0); enterNewPhrase('test1', 0);
enterNewPhrase('test2', 1); enterNewPhrase('test2', 1);
enterNewPhrase('test3', 2); enterNewPhrase('test3', 2);
const displayVal1 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test1`; enterNewPhrase('test4', 3);
const displayVal2 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[1]}: test2`; const displayVal1 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[0]}: test1`;
const displayVal3 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[2]}: test3`; const displayVal2 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[1]}: test2`;
expect(component.onPhraseChange).toHaveBeenCalled(); const displayVal3 = ` SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[2]}: test3`;
expect(component.displayValue$.next).toHaveBeenCalledWith(displayVal1 + displayVal2 + displayVal3); 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', () => { it('should set correct value and update display value', () => {
spyOn(component.displayValue$, 'next'); 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); component.setValue(searchCondition);
expect(component.getCurrentValue()).toEqual(searchCondition); expect(component.getCurrentValue()).toEqual(searchCondition);
expect(component.displayValue$.next).toHaveBeenCalled(); expect(component.displayValue$.next).toHaveBeenCalled();
}); });
it('should reset value and display value when reset is called', () => { 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); component.setValue(searchCondition);
fixture.detectChanges(); fixture.detectChanges();
spyOn(component.context, 'update'); spyOn(component.context, 'update');
@@ -125,14 +123,13 @@ describe('SearchLogicalFilterComponent', () => {
component.reset(); component.reset();
expect(component.context.queryFragments[component.id]).toBe(''); expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).toHaveBeenCalled(); 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(''); expect(component.displayValue$.next).toHaveBeenCalledWith('');
}); });
it('should form correct query from match all field', () => { it('should form correct query from match all field', () => {
spyOn(component.context, 'update'); spyOn(component.context, 'update');
enterNewPhrase('test1', 0); enterNewPhrase(' test1 test2 ', 0);
enterNewPhrase('test2', 0);
component.submitValues(); component.submitValues();
expect(component.context.update).toHaveBeenCalled(); expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test1" AND field1:"test2") OR (field2:"test1" AND field2:"test2"))'); 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', () => { it('should form correct query from match any field', () => {
spyOn(component.context, 'update'); spyOn(component.context, 'update');
enterNewPhrase('test3', 1); enterNewPhrase(' test3 test4', 1);
enterNewPhrase('test4', 1);
component.submitValues(); component.submitValues();
expect(component.context.update).toHaveBeenCalled(); expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test3" OR field1:"test4") OR (field2:"test3" OR field2:"test4"))'); 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', () => { it('should form correct query from exclude field', () => {
spyOn(component.context, 'update'); spyOn(component.context, 'update');
enterNewPhrase('test5', 2); enterNewPhrase('test5 test6 ', 2);
enterNewPhrase('test6', 2);
component.submitValues(); component.submitValues();
expect(component.context.update).toHaveBeenCalled(); 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"))'); 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', () => { it('should form correct joined query from all fields', () => {
spyOn(component.context, 'update'); spyOn(component.context, 'update');
enterNewPhrase('test1', 0); enterNewPhrase('test1', 0);
enterNewPhrase('test2', 1); enterNewPhrase('test2', 1);
enterNewPhrase('test3', 2); enterNewPhrase('test3', 2);
enterNewPhrase('test4', 3);
component.submitValues(); component.submitValues();
const subQuery1 = '((field1:"test1") OR (field2:"test1"))'; const subQuery1 = '((field1:"test1") OR (field2:"test1"))';
const subQuery2 = '((field1:"test2") OR (field2:"test2"))'; const subQuery2 = '((field1:"test2") OR (field2:"test2"))';
const subQuery3 = '((NOT field1:"test3") AND (NOT field2:"test3"))'; const subQuery3 = '((NOT field1:"test3") AND (NOT field2:"test3"))';
const subQuery4 = '((field1:"test4") OR (field2:"test4"))';
expect(component.context.update).toHaveBeenCalled(); 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}`);
}); });
}); });

View File

@@ -25,10 +25,11 @@ import { TranslationService } from '@alfresco/adf-core';
export enum LogicalSearchFields { export enum LogicalSearchFields {
MATCH_ALL = 'matchAll', MATCH_ALL = 'matchAll',
MATCH_ANY = 'matchAny', 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 // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface LogicalSearchCondition extends LogicalSearchConditionEnumValuedKeys {} export interface LogicalSearchCondition extends LogicalSearchConditionEnumValuedKeys {}
@@ -39,26 +40,22 @@ export interface LogicalSearchCondition extends LogicalSearchConditionEnumValued
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class SearchLogicalFilterComponent implements SearchWidget, OnInit { export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
private searchCondition: LogicalSearchCondition;
private reset$ = new Subject<void>();
id: string; id: string;
settings?: SearchWidgetSettings; settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService; context?: SearchQueryBuilderService;
startValue: string; startValue: string;
displayValue$: Subject<string> = new Subject<string>(); searchCondition: LogicalSearchCondition;
resetObservable = this.reset$.asObservable(); fields = Object.keys(LogicalSearchFields);
LogicalSearchFields = LogicalSearchFields; LogicalSearchFields = LogicalSearchFields;
displayValue$: Subject<string> = new Subject();
constructor(private translationService: TranslationService) {} constructor(private translationService: TranslationService) {}
ngOnInit(): void { ngOnInit(): void {
this.searchCondition = { matchAll: [], matchAny: [], exclude: [] }; this.clearSearchInputs();
this.updateDisplayValue();
} }
onPhraseChange(phrases: string[], field: LogicalSearchFields) { onInputChange() {
this.searchCondition[field] = phrases;
this.updateDisplayValue(); this.updateDisplayValue();
} }
@@ -68,11 +65,12 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
const fields = this.settings.field.split(',').map((field) => field += ':'); const fields = this.settings.field.split(',').map((field) => field += ':');
let query = ''; let query = '';
Object.keys(this.searchCondition).forEach((key) => { Object.keys(this.searchCondition).forEach((key) => {
if (this.searchCondition[key].length > 0) { if (this.searchCondition[key] !== '') {
let connector = ''; let connector = '';
let subQuery = ''; let subQuery = '';
switch(key) { switch(key) {
case LogicalSearchFields.MATCH_ALL: case LogicalSearchFields.MATCH_ALL:
case LogicalSearchFields.MATCH_EXACT:
connector = 'AND'; connector = 'AND';
break; break;
case LogicalSearchFields.MATCH_ANY: case LogicalSearchFields.MATCH_ANY:
@@ -87,12 +85,16 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
fields.forEach((field) => { fields.forEach((field) => {
subQuery += subQuery === '' ? '' : key === LogicalSearchFields.EXCLUDE ? ' AND ' : ' OR '; subQuery += subQuery === '' ? '' : key === LogicalSearchFields.EXCLUDE ? ' AND ' : ' OR ';
let fieldQuery = '('; let fieldQuery = '(';
this.searchCondition[key].forEach((phrase: string) => { if (key === LogicalSearchFields.MATCH_EXACT) {
const refinedPhrase = '\"' + phrase + '\"'; fieldQuery += field + '"' + this.searchCondition[key].trim() + '"';
fieldQuery += fieldQuery === '(' ? } else {
`${key === LogicalSearchFields.EXCLUDE ? 'NOT ' : ''}${field}${refinedPhrase}` : this.searchCondition[key].split(' ').filter((condition: string) => condition !== '').forEach((phrase: string) => {
` ${connector} ${field}${refinedPhrase}`; const refinedPhrase = '\"' + phrase + '\"';
}); fieldQuery += fieldQuery === '(' ?
`${key === LogicalSearchFields.EXCLUDE ? 'NOT ' : ''}${field}${refinedPhrase}` :
` ${connector} ${field}${refinedPhrase}`;
});
}
subQuery += `${fieldQuery})`; subQuery += `${fieldQuery})`;
}); });
query += query === '' ? `(${subQuery})` : ` AND (${subQuery})`; query += query === '' ? `(${subQuery})` : ` AND (${subQuery})`;
@@ -105,7 +107,7 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
} }
hasValidValue(): boolean { 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 { getCurrentValue(): LogicalSearchCondition {
@@ -119,9 +121,8 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
reset() { reset() {
if (this.id && this.context) { if (this.id && this.context) {
this.reset$.next();
this.context.queryFragments[this.id] = ''; this.context.queryFragments[this.id] = '';
this.updateDisplayValue(); this.clearSearchInputs();
this.context.update(); this.context.update();
} }
} }
@@ -130,13 +131,17 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
if (this.hasValidValue()) { if (this.hasValidValue()) {
const displayValue = Object.keys(this.searchCondition).reduce((acc, key) => { const displayValue = Object.keys(this.searchCondition).reduce((acc, key) => {
const fieldIndex = Object.values(LogicalSearchFields).indexOf(key as LogicalSearchFields); const fieldIndex = Object.values(LogicalSearchFields).indexOf(key as LogicalSearchFields);
const fieldKeyTranslated = this.translationService.instant(`SEARCH.LOGICAL_SEARCH.${Object.keys(LogicalSearchFields)[fieldIndex]}`); const fieldKeyTranslated = this.translationService.instant(`SEARCH.LOGICAL_SEARCH.${this.fields[fieldIndex]}`);
const stackedPhrases = this.searchCondition[key].reduce((phraseAcc, phrase) => `${phraseAcc === '' ? phraseAcc : phraseAcc + ','} ${phrase}`, ''); return this.searchCondition[key] !== '' ? `${acc} ${fieldKeyTranslated}: ${this.searchCondition[key]}` : acc;
return stackedPhrases !== '' ? `${acc} ${fieldKeyTranslated}: ${stackedPhrases}` : acc;
}, ''); }, '');
this.displayValue$.next(displayValue); this.displayValue$.next(displayValue);
} else { } else {
this.displayValue$.next(''); this.displayValue$.next('');
} }
} }
private clearSearchInputs(): void {
this.searchCondition = { matchAll: '', matchAny: '', matchExact: '', exclude: '' };
this.updateDisplayValue();
}
} }

View File

@@ -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-chips.component';
export * from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.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-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/search-logical-filter/search-logical-filter.component';
export * from './components/reset-search.directive'; export * from './components/reset-search.directive';
export * from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component'; export * from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component';

View File

@@ -49,7 +49,6 @@ import { SearchFilterMenuCardComponent } from './components/search-filter-chips/
import { SearchFacetFieldComponent } from './components/search-facet-field/search-facet-field.component'; 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 { 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 { 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 { SearchLogicalFilterComponent } from './components/search-logical-filter/search-logical-filter.component';
import { ResetSearchDirective } from './components/reset-search.directive'; import { ResetSearchDirective } from './components/reset-search.directive';
@@ -88,7 +87,6 @@ import { ResetSearchDirective } from './components/reset-search.directive';
SearchFacetFieldComponent, SearchFacetFieldComponent,
SearchWidgetChipComponent, SearchWidgetChipComponent,
SearchFacetChipComponent, SearchFacetChipComponent,
SearchChipInputComponent,
SearchLogicalFilterComponent, SearchLogicalFilterComponent,
ResetSearchDirective ResetSearchDirective
], ],
@@ -115,7 +113,6 @@ import { ResetSearchDirective } from './components/reset-search.directive';
SearchFilterChipsComponent, SearchFilterChipsComponent,
SearchFilterMenuCardComponent, SearchFilterMenuCardComponent,
SearchFacetFieldComponent, SearchFacetFieldComponent,
SearchChipInputComponent,
SearchLogicalFilterComponent, SearchLogicalFilterComponent,
ResetSearchDirective ResetSearchDirective
], ],