diff --git a/docs/search-control.component.md b/docs/search-control.component.md index b685ee4842..1921e746d7 100644 --- a/docs/search-control.component.md +++ b/docs/search-control.component.md @@ -1,4 +1,4 @@ -# Search component +# Search control component Displays a input text which shows find-as-you-type suggestions. @@ -38,10 +38,8 @@ Displays a input text which shows find-as-you-type suggestions. | highlight | boolean | false | Use the true value if you want to see the searched word highlighted. | | expandable | boolean | true | Whether to use an expanding search control, if false then a regular input is used. | | liveSearchEnabled | boolean | true | Whether find-as-you-type suggestions should be offered for matching content items. Set to false to disable. | -| liveSearchRoot | string | "-root-" | NodeRef or node name where the search should start. | -| liveSearchResultType | string | | Node type to filter live search results by, e.g. 'cm:content'. | | liveSearchMaxResults | number | 5 | Maximum number of results to show in the live search. | -| liveSearchResultSort | string | | Criteria to sort live search results by, must be one of "name" , "modifiedAt" or "createdAt" | +| customSearchNode | [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api | ### Events diff --git a/docs/search.component.md b/docs/search.component.md index ca7fb2d415..7713dc08f9 100644 --- a/docs/search.component.md +++ b/docs/search.component.md @@ -1,4 +1,4 @@ -# Search Results component +# Search component @@ -29,11 +29,10 @@ | Name | Type | Default | Description | | --- | --- | --- | --- | | searchTerm | string | | Search term to use when executing the search. Updating this value will run a new search and update the results | -| rootNodeId | string | "-root-" | NodeRef or node name where the search should start. | -| resultType | string | | Node type to filter search results by, e.g. 'cm:content', 'cm:folder' if you want only the files. | | maxResults | number | 20 | Maximum number of results to show in the search. | -| resultSort | string | | Criteria to sort search results by, must be one of "name" , "modifiedAt" or "createdAt" | +| skipResults | number | 0 | Number of results to skip from the results pagination. | | displayWith | function | | Function that maps an option's value to its display value in the trigger | +| searchNode | [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api | ### Events diff --git a/lib/content-services/mock/search.component.mock.ts b/lib/content-services/mock/search.component.mock.ts index 86fd527143..143e9871c2 100644 --- a/lib/content-services/mock/search.component.mock.ts +++ b/lib/content-services/mock/search.component.mock.ts @@ -17,6 +17,7 @@ import { Component, ViewChild } from '@angular/core'; import { SearchComponent } from '../search/components/search.component'; +import { QueryBody } from 'alfresco-js-api'; const entryItem = { entry: { @@ -117,7 +118,7 @@ export let errorJson = { @Component({ template: ` - @@ -142,6 +143,7 @@ export let errorJson = { message: string = ''; searchedWord= ''; maxResults: number = 5; + searchNode: QueryBody; constructor() { } @@ -158,6 +160,10 @@ export let errorJson = { this.searchedWord = str; } + setSearchNodeTo(searchNode: QueryBody) { + this.searchNode = searchNode; + } + changeMaxResultTo(newMax: number) { this.maxResults = newMax; } diff --git a/lib/content-services/search/components/search-control.component.html b/lib/content-services/search/components/search-control.component.html index 2c7fe95f0a..f5db53e433 100644 --- a/lib/content-services/search/components/search-control.component.html +++ b/lib/content-services/search/components/search-control.component.html @@ -25,9 +25,7 @@ diff --git a/lib/content-services/search/components/search-control.component.spec.ts b/lib/content-services/search/components/search-control.component.spec.ts index 3f3eb593e0..736d71f560 100644 --- a/lib/content-services/search/components/search-control.component.spec.ts +++ b/lib/content-services/search/components/search-control.component.spec.ts @@ -19,7 +19,7 @@ import { DebugElement } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MaterialModule } from '../../material.module'; import { By } from '@angular/platform-browser'; -import { AuthenticationService, SearchService } from '@alfresco/adf-core'; +import { AuthenticationService, SearchApiService } from '@alfresco/adf-core'; import { ThumbnailService } from '@alfresco/adf-core'; import { Observable } from 'rxjs/Observable'; import { noResult, results } from '../../mock'; @@ -27,13 +27,13 @@ import { SearchControlComponent } from './search-control.component'; import { SearchTriggerDirective } from './search-trigger.directive'; import { SearchComponent } from './search.component'; -xdescribe('SearchControlComponent', () => { +describe('SearchControlComponent', () => { let fixture: ComponentFixture; let component: SearchControlComponent; let element: HTMLElement; let debugElement: DebugElement; - let searchService: SearchService; + let searchService: SearchApiService; let authService: AuthenticationService; beforeEach(async(() => { @@ -48,12 +48,12 @@ xdescribe('SearchControlComponent', () => { ], providers: [ ThumbnailService, - SearchService + SearchApiService ] }).compileComponents().then(() => { fixture = TestBed.createComponent(SearchControlComponent); debugElement = fixture.debugElement; - searchService = TestBed.get(SearchService); + searchService = TestBed.get(SearchApiService); authService = TestBed.get(AuthenticationService); component = fixture.componentInstance; element = fixture.nativeElement; @@ -76,14 +76,14 @@ xdescribe('SearchControlComponent', () => { })); it('should emit searchChange when search term input changed', async(() => { - spyOn(searchService, 'getNodeQueryResults').and.callFake(() => { - return Observable.of({ entry: { list: []}}); + spyOn(searchService, 'search').and.callFake(() => { + return Observable.of({ entry: { list: [] } }); }); component.searchChange.subscribe(value => { expect(value).toBe('customSearchTerm'); }); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'customSearchTerm'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -91,12 +91,12 @@ xdescribe('SearchControlComponent', () => { })); it('should update FAYT search when user inputs a valid term', async(() => { - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'customSearchTerm'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -107,13 +107,13 @@ xdescribe('SearchControlComponent', () => { }); })); - it('should NOT update FAYT term when user inputs a search term less than 3 characters', async(() => { - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); - inputDebugElement.nativeElement.value = 'cu'; + it('should NOT update FAYT term when user inputs an empty string as search term ', async(() => { + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); + inputDebugElement.nativeElement.value = ''; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -126,7 +126,7 @@ xdescribe('SearchControlComponent', () => { component.searchChange.subscribe(value => { expect(value).toBe('cu'); }); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'cu'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -184,10 +184,10 @@ xdescribe('SearchControlComponent', () => { }); spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -206,10 +206,10 @@ xdescribe('SearchControlComponent', () => { it('should make autocomplete list control visible when search box has focus and there is a search result', (done) => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -224,10 +224,10 @@ xdescribe('SearchControlComponent', () => { it('should show autocomplete list noe results when search box has focus and there is search result with length 0', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(noResult)); + spyOn(searchService, 'search').and.returnValue(Observable.of(noResult)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'NO RES'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -241,10 +241,10 @@ xdescribe('SearchControlComponent', () => { it('should hide autocomplete list results when the search box loses focus', (done) => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'NO RES'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -264,10 +264,10 @@ xdescribe('SearchControlComponent', () => { it('should keep autocomplete list control visible when user tabs into results', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -280,39 +280,89 @@ xdescribe('SearchControlComponent', () => { inputDebugElement.nativeElement.dispatchEvent(new KeyboardEvent('keypress', { key: 'TAB' })); fixture.detectChanges(); - expect(element.querySelector('#autocomplete-search-result-list') ).not.toBeNull(); + expect(element.querySelector('#autocomplete-search-result-list')).not.toBeNull(); + }); + })); + + it('should close the autocomplete when user press ESCAPE', async(() => { + spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); + fixture.detectChanges(); + + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); + inputDebugElement.nativeElement.value = 'TEST'; + inputDebugElement.nativeElement.focus(); + inputDebugElement.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let resultElement: HTMLElement = element.querySelector('#result_option_0'); + expect(resultElement).not.toBeNull(); + let escapeEvent: any = new Event('ESCAPE'); + escapeEvent.keyCode = 27; + inputDebugElement.triggerEventHandler('keydown', escapeEvent); + fixture.whenStable().then(() => { + fixture.detectChanges(); + resultElement = element.querySelector('#result_option_0'); + expect(resultElement).toBeNull(); + }); + }); + })); + + it('should close the autocomplete when user press ENTER on input', async(() => { + spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); + fixture.detectChanges(); + + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); + inputDebugElement.nativeElement.value = 'TEST'; + inputDebugElement.nativeElement.focus(); + inputDebugElement.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let resultElement: HTMLElement = element.querySelector('#result_option_0'); + expect(resultElement).not.toBeNull(); + let escapeEvent: any = new Event('ENTER'); + escapeEvent.keyCode = 13; + inputDebugElement.triggerEventHandler('keydown', escapeEvent); + fixture.whenStable().then(() => { + fixture.detectChanges(); + resultElement = element.querySelector('#result_option_0'); + expect(resultElement).toBeNull(); + }); }); })); it('should focus input element when autocomplete list is cancelled', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); let escapeEvent: any = new Event('ESCAPE'); escapeEvent.keyCode = 27; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(escapeEvent); fixture.detectChanges(); fixture.whenStable().then(() => { - expect(element.querySelector('#result_name_0') ).toBeNull(); + expect(element.querySelector('#result_name_0')).toBeNull(); expect(document.activeElement.id).toBe(inputDebugElement.nativeElement.id); }); })); it('should NOT display a autocomplete list control when configured not to', async(() => { - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); component.liveSearchEnabled = false; fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); fixture.whenStable().then(() => { fixture.detectChanges(); - expect(element.querySelector('#autocomplete-search-result-list') ).toBeNull(); + expect(element.querySelector('#autocomplete-search-result-list')).toBeNull(); }); })); }); @@ -320,41 +370,58 @@ xdescribe('SearchControlComponent', () => { describe('search button', () => { it('should NOT display a autocomplete list control when configured not to', async(() => { + fixture.detectChanges(); + let searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); component.subscriptAnimationState = 'active'; fixture.detectChanges(); - - let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button')); + expect(component.subscriptAnimationState).toBe('active'); searchButton.triggerEventHandler('click', null); - fixture.whenStable().then(() => { + window.setTimeout(() => { fixture.detectChanges(); expect(component.subscriptAnimationState).toBe('inactive'); - }); + }, 100); })); it('click on the search button should open the input box when is close', (done) => { - component.subscriptAnimationState = 'inactive'; fixture.detectChanges(); - let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button')); + let searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); searchButton.triggerEventHandler('click', null); window.setTimeout(() => { fixture.detectChanges(); expect(component.subscriptAnimationState).toBe('active'); done(); - }, 200); + }, 100); }); it('Search button should not change the input state too often', async(() => { + fixture.detectChanges(); + let searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); component.subscriptAnimationState = 'active'; fixture.detectChanges(); - let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button')); + expect(component.subscriptAnimationState).toBe('active'); searchButton.triggerEventHandler('click', null); fixture.detectChanges(); searchButton.triggerEventHandler('click', null); fixture.detectChanges(); - fixture.whenStable().then(() => { + window.setTimeout(() => { + fixture.detectChanges(); expect(component.subscriptAnimationState).toBe('inactive'); - }); + }, 100); + })); + + it('Search bar should close when user press ESC button', async(() => { + fixture.detectChanges(); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); + component.subscriptAnimationState = 'active'; + fixture.detectChanges(); + expect(component.subscriptAnimationState).toBe('active'); + + inputDebugElement.triggerEventHandler('keyup.escape', {}); + window.setTimeout(() => { + fixture.detectChanges(); + expect(component.subscriptAnimationState).toBe('inactive'); + }, 100); })); }); @@ -362,26 +429,26 @@ xdescribe('SearchControlComponent', () => { it('should emit a option clicked event when item is clicked', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); component.optionClicked.subscribe((item) => { expect(item.entry.id).toBe('123'); }); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); - let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0')); + let firstOption: DebugElement = debugElement.query(By.css('#result_name_0')); firstOption.triggerEventHandler('click', null); }); })); it('should set deactivate the search after element is clicked', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); component.optionClicked.subscribe((item) => { window.setTimeout(() => { expect(component.subscriptAnimationState).toBe('inactive'); @@ -389,27 +456,27 @@ xdescribe('SearchControlComponent', () => { }); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); fixture.whenStable().then(() => { fixture.detectChanges(); - let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0')); + let firstOption: DebugElement = debugElement.query(By.css('#result_name_0')); firstOption.triggerEventHandler('click', null); }); })); it('should NOT reset the search term after element is clicked', async(() => { spyOn(component, 'isSearchBarActive').and.returnValue(true); - spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results)); + spyOn(searchService, 'search').and.returnValue(Observable.of(results)); component.optionClicked.subscribe((item) => { expect(component.searchTerm).not.toBeFalsy(); expect(component.searchTerm).toBe('TEST'); }); fixture.detectChanges(); - let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input')); + let inputDebugElement = debugElement.query(By.css('#adf-control-input')); inputDebugElement.nativeElement.value = 'TEST'; inputDebugElement.nativeElement.focus(); inputDebugElement.nativeElement.dispatchEvent(new Event('input')); @@ -417,7 +484,7 @@ xdescribe('SearchControlComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); - let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0')); + let firstOption: DebugElement = debugElement.query(By.css('#result_name_0')); firstOption.triggerEventHandler('click', null); }); })); diff --git a/lib/content-services/search/components/search-control.component.ts b/lib/content-services/search/components/search-control.component.ts index f724d2634a..ca007b4f2a 100644 --- a/lib/content-services/search/components/search-control.component.ts +++ b/lib/content-services/search/components/search-control.component.ts @@ -18,7 +18,7 @@ import { AuthenticationService, ThumbnailService } from '@alfresco/adf-core'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { MinimalNodeEntity } from 'alfresco-js-api'; +import { MinimalNodeEntity, QueryBody } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @@ -55,18 +55,12 @@ export class SearchControlComponent implements OnInit, OnDestroy { @Input() liveSearchEnabled: boolean = true; - @Input() - liveSearchRoot: string = '-root-'; - - @Input() - liveSearchResultType: string = null; - - @Input() - liveSearchResultSort: string = null; - @Input() liveSearchMaxResults: number = 5; + @Input() + customSearchNode: QueryBody; + @Output() submit: EventEmitter = new EventEmitter(); diff --git a/lib/content-services/search/components/search-trigger.directive.ts b/lib/content-services/search/components/search-trigger.directive.ts index 8ed41f9278..0b0fcb22cd 100644 --- a/lib/content-services/search/components/search-trigger.directive.ts +++ b/lib/content-services/search/components/search-trigger.directive.ts @@ -46,8 +46,6 @@ export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = { multi: true }; -const MIN_WORD_LENGTH_VALID = 3; - @Directive({ selector: `input[searchAutocomplete], textarea[searchAutocomplete]`, host: { @@ -82,6 +80,9 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { ngOnDestroy() { this.escapeEventStream.unsubscribe(); + if ( this.closingActionsSubscription ) { + this.closingActionsSubscription.unsubscribe(); + } } get panelOpen(): boolean { @@ -160,11 +161,11 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { if (document.activeElement === event.target) { let inputValue: string = (event.target as HTMLInputElement).value; this.onChange(inputValue); - if (inputValue.length >= MIN_WORD_LENGTH_VALID) { + if (inputValue) { this.searchPanel.keyPressedStream.next(inputValue); this.openPanel(); } else { - this.searchPanel.resetResults(); + this.closePanel(); } } } @@ -193,7 +194,6 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { this.searchPanel.setVisibility(); return this.panelClosingActions; }) - .first() .subscribe(event => this.setValueAndClose(event)); } diff --git a/lib/content-services/search/components/search.component.spec.ts b/lib/content-services/search/components/search.component.spec.ts index 61d4afb82a..6853dec7aa 100644 --- a/lib/content-services/search/components/search.component.spec.ts +++ b/lib/content-services/search/components/search.component.spec.ts @@ -16,27 +16,40 @@ */ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { SearchService } from '@alfresco/adf-core'; +import { SearchApiService } from '@alfresco/adf-core'; +import { QueryBody } from 'alfresco-js-api'; +import { Observable } from 'rxjs/Observable'; import { SearchModule } from '../../index'; -import { differentResult, result, SimpleSearchTestComponent } from '../../mock'; +import { differentResult, folderResult, result, SimpleSearchTestComponent } from '../../mock'; + +function fakeNodeResultSearch(searchNode: QueryBody): Observable { + if (searchNode.query.query === 'FAKE_SEARCH_EXMPL') { + return Observable.of(differentResult); + } + if (searchNode.filterQueries.length === 1 && + searchNode.filterQueries[0].query === "TYPE:'cm:folder'") { + return Observable.of(folderResult); + } + return Observable.of(result); +} describe('SearchComponent', () => { let fixture: ComponentFixture, element: HTMLElement; let component: SimpleSearchTestComponent; - let searchService: SearchService; + let searchService: SearchApiService; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ SearchModule ], - declarations: [ SimpleSearchTestComponent ] + declarations: [SimpleSearchTestComponent] }).compileComponents().then(() => { fixture = TestBed.createComponent(SimpleSearchTestComponent); component = fixture.componentInstance; element = fixture.nativeElement; - searchService = TestBed.get(SearchService); + searchService = TestBed.get(SearchApiService); }); })); @@ -47,8 +60,8 @@ describe('SearchComponent', () => { }); it('should clear results straight away when a new search term is entered', async(() => { - spyOn(searchService, 'getQueryNodesPromise') - .and.returnValues(Promise.resolve(result), Promise.resolve(differentResult)); + spyOn(searchService, 'search') + .and.returnValues(Observable.of(result), Observable.of(differentResult)); component.setSearchWordTo('searchTerm'); fixture.detectChanges(); @@ -67,8 +80,8 @@ describe('SearchComponent', () => { })); it('should display the returned search results', async(() => { - spyOn(searchService, 'getQueryNodesPromise') - .and.returnValue(Promise.resolve(result)); + spyOn(searchService, 'search') + .and.returnValue(Observable.of(result)); component.setSearchWordTo('searchTerm'); fixture.detectChanges(); @@ -80,8 +93,8 @@ describe('SearchComponent', () => { })); it('should emit error event when search call fail', async(() => { - spyOn(searchService, 'getQueryNodesPromise') - .and.returnValue(Promise.reject({ status: 402 })); + spyOn(searchService, 'search') + .and.returnValue(Observable.fromPromise(Promise.reject({ status: 402 }))); component.setSearchWordTo('searchTerm'); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -92,8 +105,8 @@ describe('SearchComponent', () => { })); it('should be able to hide the result panel', async(() => { - spyOn(searchService, 'getQueryNodesPromise') - .and.returnValues(Promise.resolve(result), Promise.resolve(differentResult)); + spyOn(searchService, 'search') + .and.returnValues(Observable.of(result), Observable.of(differentResult)); component.setSearchWordTo('searchTerm'); fixture.detectChanges(); @@ -111,4 +124,70 @@ describe('SearchComponent', () => { }); })); }); + + describe('search node', () => { + + afterEach(() => { + fixture.destroy(); + }); + + it('should perform a search based on the query node given', async(() => { + spyOn(searchService, 'search') + .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); + let fakeSearchNode: QueryBody = { + query: { + query: '' + }, + filterQueries: [ + { 'query': "TYPE:'cm:folder'" } + ] + }; + component.setSearchNodeTo(fakeSearchNode); + component.setSearchWordTo('searchTerm'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length; + expect(optionShowed).toBe(1); + let folderOption: HTMLElement = element.querySelector('#result_option_0'); + expect(folderOption.textContent.trim()).toBe('MyFolder'); + }); + })); + + it('should perform a search with a defaultNode if no searchnode is given', async(() => { + spyOn(searchService, 'search') + .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); + component.setSearchWordTo('searchTerm'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length; + expect(optionShowed).toBe(1); + let folderOption: HTMLElement = element.querySelector('#result_option_0'); + expect(folderOption.textContent.trim()).toBe('MyDoc'); + }); + })); + + it('should perform a search with the searchNode given', async(() => { + spyOn(searchService, 'search') + .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); + let fakeSearchNode: QueryBody = { + query: { + query: 'FAKE_SEARCH_EXMPL' + }, + filterQueries: [ + { 'query': "TYPE:'cm:folder'" } + ] + }; + component.setSearchNodeTo(fakeSearchNode); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length; + expect(optionShowed).toBe(1); + let folderOption: HTMLElement = element.querySelector('#result_option_0'); + expect(folderOption.textContent.trim()).toBe('TEST_DOC'); + }); + })); + }); }); diff --git a/lib/content-services/search/components/search.component.ts b/lib/content-services/search/components/search.component.ts index c23c1f6c43..eee6801003 100644 --- a/lib/content-services/search/components/search.component.ts +++ b/lib/content-services/search/components/search.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { SearchOptions, SearchService } from '@alfresco/adf-core'; +import { SearchApiService } from '@alfresco/adf-core'; import { AfterContentInit, ChangeDetectionStrategy, @@ -31,7 +31,7 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { NodePaging } from 'alfresco-js-api'; +import { NodePaging, QueryBody } from 'alfresco-js-api'; import { Subject } from 'rxjs/Subject'; @Component({ @@ -61,17 +61,14 @@ export class SearchComponent implements AfterContentInit, OnChanges { maxResults: number = 20; @Input() - resultSort: string = null; - - @Input() - rootNodeId: string = '-root-'; - - @Input() - resultType: string = null; + skipResults: number = 0; @Input() searchTerm: string = ''; + @Input() + searchNode: QueryBody; + @Input('class') set classList(classList: string) { if (classList && classList.length) { @@ -104,7 +101,7 @@ export class SearchComponent implements AfterContentInit, OnChanges { _classList: { [key: string]: boolean } = {}; constructor( - private searchService: SearchService, + private searchService: SearchApiService, private changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef) { this.keyPressedStream.asObservable() @@ -119,10 +116,13 @@ export class SearchComponent implements AfterContentInit, OnChanges { } ngOnChanges(changes) { - if (changes.searchTerm) { - this.resetResults(); + this.resetResults(); + if (changes.searchTerm && changes.searchTerm.currentValue) { this.displaySearchResults(changes.searchTerm.currentValue); } + if (changes.searchNode && changes.searchNode.currentValue) { + this.displaySearchResults(); + } } resetResults() { @@ -140,18 +140,17 @@ export class SearchComponent implements AfterContentInit, OnChanges { } } - private displaySearchResults(searchTerm) { - let searchOpts: SearchOptions = { - include: ['path', 'allowableOperations'], - rootNodeId: this.rootNodeId, - nodeType: this.resultType, - maxItems: this.maxResults, - orderBy: this.resultSort - }; - if (searchTerm !== null && searchTerm !== '') { + private hasValidSearchQuery(searchOpts: QueryBody) { + return searchOpts && searchOpts.query && searchOpts.query.query; + } + + private displaySearchResults(searchTerm?: string) { + let searchOpts: QueryBody = this.getSearchNode(searchTerm); + + if (this.hasValidSearchQuery(searchOpts)) { searchTerm = searchTerm + '*'; this.searchService - .getNodeQueryResults(searchTerm, searchOpts) + .search(searchOpts) .subscribe( results => { this.results = results; @@ -165,9 +164,39 @@ export class SearchComponent implements AfterContentInit, OnChanges { this.error.emit(error); } }); + } else { + this.cleanResults(); } } + private getSearchNode(searchTerm: string): QueryBody { + if (this.searchNode) { + if (!this.searchNode.query.query && searchTerm) { + this.searchNode.query.query = searchTerm; + } + return this.searchNode; + } else { + return this.generateDefaultSearchNode(searchTerm); + } + } + + private generateDefaultSearchNode(searchTerm: string): QueryBody { + let defaultSearchNode: QueryBody = { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: this.maxResults.toString(), + skipCount: this.skipResults.toString() + }, + filterQueries: [ + { query: "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { query: 'NOT cm:creator:System' }] + }; + return defaultSearchNode; + } + hidePanel() { if (this.isOpen) { this._classList['adf-search-show'] = false;