Merge pull request #1006 from Alfresco/dev-wabson-555

Update search component to work with 0.4.0 JS API
This commit is contained in:
Mario Romano 2016-11-08 13:49:18 +00:00 committed by GitHub
commit bc823b829b
13 changed files with 175 additions and 67 deletions

View File

@ -1,5 +1,12 @@
<alfresco-search-control *ngIf="isLoggedIn()" [searchTerm]="searchTerm" [autocomplete]="false"
(searchSubmit)="onSearchSubmit($event);" (searchChange)="onSearchTermChange($event);" (expand)="onExpandToggle($event);" (fileSelect)="onFileClicked($event)"></alfresco-search-control>
<alfresco-search-control *ngIf="isLoggedIn()"
[searchTerm]="searchTerm"
[autocomplete]="false"
[liveSearchResultType]="'cm:content'"
(searchSubmit)="onSearchSubmit($event);"
(searchChange)="onSearchTermChange($event);"
(expand)="onExpandToggle($event);"
(fileSelect)="onFileClicked($event)">
</alfresco-search-control>
<alfresco-viewer [(showViewer)]="fileShowed"
[fileNodeId]="fileNodeId"

View File

@ -1,6 +1,6 @@
<div class="search-results-container">
<h1>Search results</h1>
<alfresco-search (preview)="onFileClicked($event)"></alfresco-search>
<alfresco-search [resultType]="'cm:content'" (preview)="onFileClicked($event)"></alfresco-search>
</div>
<alfresco-viewer [(showViewer)]="fileShowed" [fileNodeId]="fileNodeId" [overlayMode]="true">

View File

@ -173,16 +173,20 @@ bootstrap(SearchDemo, [
**searchChange**: Emitted when the search term is changed. The search term is provided in the 'value' property of the returned object. If the term is at less than three characters in length then the term is truncated to an empty string.<br />
**searchSubmit**: Emitted when the search form is submitted. The search term is provided in the 'value' property of the returned object.<br />
**fileSelect**: Emitted when a file item from the list of find-as-you-type results is selected
**fileSelect**: Emitted when a file item from the list of find-as-you-type results is selected<br />
**expand**: Emitted when the expanded state of the control changes based on focus events and the content of the input control
#### Options
**searchTerm**: {string} (optional) default "". Search term to pre-populate the field with<br />
**inputType**: {string} (optional) default "text". Type of the input field to render, e.g. "search" or "text" (default)<br />
**expandable** {boolean} (optional) default true. Whether to use an expanding search control, if false then a regular input is used.
**autocomplete** {boolean} (optional) default true. Whether the browser should offer field auto-completion for the input field to the user.
**autocompleteEnabled** {boolean} (optional) default true. Whether find-as-you-type suggestions should be offered for matching content items. Set to false to disable.
**expandable** {boolean} (optional) default true. Whether to use an expanding search control, if false then a regular input is used.<br />
**autocomplete** {boolean} (optional) default true. Whether the browser should offer field auto-completion for the input field to the user.<br />
**liveSearchEnabled** {boolean} (optional) default true. Whether find-as-you-type suggestions should be offered for matching content items. Set to false to disable.<br />
**liveSearchRoot** {boolean} (optional) default "-root-". NodeRef or node name where the search should start.<br />
**liveSearchResultType** {boolean} (optional) default (none). Node type to filter live search results by, e.g. 'cm:content'.<br />
**liveSearchMaxResults** {boolean} (optional) default 5. Maximum number of results to show in the live search.<br />
**liveSearchResultSort** {boolean} (optional) default (none). Criteria to sort live search results by, must be one of "name" , "modifiedAt" or "createdAt"
### Search results
@ -331,12 +335,17 @@ bootstrap(SearchDemo, [
#### Events
None
**preview**: Emitted when a file result is clicked/selected<br />
**resultsLoad**: Emitted when search results have fully loaded
#### Options
**searchTerm**: {string} (optional) default "". Search term to use when executing the search. Updating this value will
run a new search and update the results.<br />
**rootNodeId** {boolean} (optional) default "-root-". NodeRef or node name where the search should start.<br />
**resultType** {boolean} (optional) default (none). Node type to filter search results by, e.g. 'cm:content'.<br />
**maxResults** {boolean} (optional) default 20. Maximum number of results to show in the search.<br />
**resultSort** {boolean} (optional) default (none). Criteria to sort search results by, must be one of "name" , "modifiedAt" or "createdAt"
## Build from sources

View File

@ -56,8 +56,8 @@ export var fakeError = {
export var fakeApi = {
core: {
searchApi: {
liveSearchNodes: (term, opts) => Promise.resolve(fakeSearch)
queriesApi: {
findNodes: (term, opts) => Promise.resolve(fakeSearch)
}
}
};

View File

@ -80,7 +80,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should clear results straight away when a new search term is entered', async(() => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.searchTerm = 'searchTerm';
@ -97,7 +97,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should display the returned search results', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
@ -112,7 +112,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should limit the number of returned search results to the configured maximum', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(results));
component.resultsLoad.subscribe(() => {
@ -127,7 +127,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should display the correct thumbnail for result items', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.baseComponentPath = 'http://localhost';
@ -150,7 +150,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should display no result if no result are returned', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(noResult));
component.resultsLoad.subscribe(() => {
@ -170,7 +170,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.reject(errorJson));
});
@ -215,7 +215,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should emit file select when file item clicked', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
@ -232,7 +232,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
it('should not emit preview if a non-file item is clicked', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.fileSelect, 'emit');
@ -254,7 +254,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(results));
});
@ -366,7 +366,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
});

View File

@ -16,7 +16,7 @@
*/
import { Component, ElementRef, EventEmitter, Input, OnInit, OnChanges, Output, ViewChild } from '@angular/core';
import { AlfrescoSearchService } from './../services/alfresco-search.service';
import { AlfrescoSearchService, SearchOptions } from './../services/alfresco-search.service';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
@ -43,6 +43,15 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
@Input()
maxResults: number = 5;
@Input()
resultSort: string = null;
@Input()
rootNodeId: string = '-root';
@Input()
resultType: string = null;
@Output()
fileSelect: EventEmitter<any> = new EventEmitter();
@ -84,9 +93,16 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
* @param searchTerm Search query entered by user
*/
private displaySearchResults(searchTerm) {
let searchOpts: SearchOptions = {
include: ['path'],
rootNodeId: this.rootNodeId,
nodeType: this.resultType,
maxItems: this.maxResults,
orderBy: this.resultSort
};
if (searchTerm !== null && searchTerm !== '') {
this.alfrescoSearchService
.getLiveSearchResults(searchTerm)
.getNodeQueryResults(searchTerm, searchOpts)
.subscribe(
results => {
this.results = results.list.entries.slice(0, this.maxResults);

View File

@ -22,8 +22,13 @@
</div>
</div>
</form>
<alfresco-search-autocomplete #autocomplete *ngIf="autocompleteEnabled"
[searchTerm]="autocompleteSearchTerm" [ngClass]="{active: searchActive, valid: searchValid}"
<alfresco-search-autocomplete #autocomplete *ngIf="liveSearchEnabled"
[searchTerm]="liveSearchTerm"
[rootNodeId]="liveSearchRoot"
[resultType]="liveSearchResultType"
[resultSort]="liveSearchResultSort"
[maxResults]="liveSearchMaxResults"
[ngClass]="{active: searchActive, valid: searchValid}"
(fileSelect)="onFileClicked($event)"
(searchFocus)="onAutoCompleteFocus($event)"
(scrollBack)="onAutoCompleteReturn($event)"

View File

@ -176,7 +176,7 @@ describe('AlfrescoSearchControlComponent', () => {
it('should keep find-as-you-type control visible when user tabs into results', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
fixture.detectChanges();
@ -219,7 +219,7 @@ describe('AlfrescoSearchControlComponent', () => {
it('should select the first result in find-as-you-type when down arrow is pressed and FAYT is visible', (done) => {
fixture.detectChanges();
spyOn(component.autocompleteComponent, 'focusResult');
spyOn(component.liveSearchComponent, 'focusResult');
fixture.detectChanges();
inputEl.dispatchEvent(new Event('focus'));
window.setTimeout(() => { // wait for debounce() to complete
@ -228,7 +228,7 @@ describe('AlfrescoSearchControlComponent', () => {
key: 'ArrowDown'
}));
fixture.detectChanges();
expect(component.autocompleteComponent.focusResult).toHaveBeenCalled();
expect(component.liveSearchComponent.focusResult).toHaveBeenCalled();
done();
}, 100);
});
@ -254,7 +254,7 @@ describe('AlfrescoSearchControlComponent', () => {
});
it('should NOT display a find-as-you-type control when configured not to', () => {
fixture.componentInstance.autocompleteEnabled = false;
fixture.componentInstance.liveSearchEnabled = false;
fixture.detectChanges();
let autocomplete: Element = element.querySelector('alfresco-search-autocomplete');
expect(autocomplete).toBeNull();

View File

@ -56,16 +56,29 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
searchControl: FormControl;
@ViewChild('searchInput', {}) searchInput: ElementRef;
@ViewChild('searchInput', {})
searchInput: ElementRef;
@ViewChild('autocomplete')
autocompleteComponent: AlfrescoSearchAutocompleteComponent;
@ViewChild(AlfrescoSearchAutocompleteComponent)
liveSearchComponent: AlfrescoSearchAutocompleteComponent;
@Input()
autocompleteEnabled = true;
liveSearchEnabled: boolean = true;
@Input()
autocompleteSearchTerm = '';
liveSearchTerm: string = '';
@Input()
liveSearchRoot: string = '-root-';
@Input()
liveSearchResultType: string = 'cm:content';
@Input()
liveSearchResultSort: string = null;
@Input()
liveSearchMaxResults: number = 5;
searchActive = false;
@ -99,7 +112,7 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
private onSearchTermChange(value: string): void {
this.setAutoCompleteDisplayed(true);
this.autocompleteSearchTerm = value;
this.liveSearchTerm = value;
this.searchControl.setValue(value, true);
this.searchValid = this.searchControl.valid;
this.searchChange.emit({
@ -195,7 +208,7 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
onArrowDown(): void {
if (this.isAutoCompleteDisplayed()) {
this.autocompleteComponent.focusResult();
this.liveSearchComponent.focusResult();
} else {
this.setAutoCompleteDisplayed(true);
}

View File

@ -154,17 +154,40 @@ describe('AlfrescoSearchComponent', () => {
expect(translationService.addTranslationFolder).toHaveBeenCalledWith('node_modules/ng2-alfresco-search/dist/src');
});
describe('Rendering search results', () => {
describe('Search results', () => {
it('should call search service with the correct parameters', (done) => {
let searchTerm = 'searchTerm63688', options = {
include: ['path'],
rootNodeId: '-my-',
nodeType: 'my:type',
maxItems: 20,
orderBy: null
};
component.searchTerm = searchTerm;
component.rootNodeId = '-my-';
component.resultType = 'my:type';
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
fixture.detectChanges();
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(searchService.getQueryNodesPromise).toHaveBeenCalledWith(searchTerm, options);
done();
});
});
it('should display search results when a search term is provided', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(searchService.getSearchNodesPromise).toHaveBeenCalled();
expect(searchService.getQueryNodesPromise).toHaveBeenCalled();
expect(element.querySelector('#result_user_0')).not.toBeNull();
expect(element.querySelector('#result_user_0').innerHTML).toBe('John Doe');
expect(element.querySelector('#result_name_0').innerHTML).toBe('MyDoc');
@ -178,7 +201,7 @@ describe('AlfrescoSearchComponent', () => {
it('should display no result if no result are returned', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(noResult));
component.resultsLoad.subscribe(() => {
@ -194,7 +217,7 @@ describe('AlfrescoSearchComponent', () => {
it('should display an error if an error is encountered running the search', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.reject(errorJson));
component.resultsLoad.subscribe(() => {}, () => {
@ -214,12 +237,12 @@ describe('AlfrescoSearchComponent', () => {
it('should update search results when the search term input is changed', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(searchService.getSearchNodesPromise).toHaveBeenCalledWith('searchTerm2');
expect(searchService.getQueryNodesPromise.calls.mostRecent().args[0]).toBe('searchTerm2');
expect(element.querySelector('#result_user_0')).not.toBeNull();
expect(element.querySelector('#result_user_0').innerHTML).toBe('John Doe');
expect(element.querySelector('#result_name_0').innerHTML).toBe('MyDoc');
@ -235,7 +258,7 @@ describe('AlfrescoSearchComponent', () => {
it('should emit preview when file item clicked', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
@ -254,7 +277,7 @@ describe('AlfrescoSearchComponent', () => {
it('should not emit preview when non-file item is clicked', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.preview, 'emit');

View File

@ -17,7 +17,7 @@
import { Component, EventEmitter, Input, Output, Optional, OnChanges, SimpleChanges, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { AlfrescoSearchService } from './../services/alfresco-search.service';
import { AlfrescoSearchService, SearchOptions } from './../services/alfresco-search.service';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
@ -34,6 +34,18 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
@Input()
searchTerm: string = '';
@Input()
maxResults: number = 20;
@Input()
resultSort: string = null;
@Input()
rootNodeId: string = '-root-';
@Input()
resultType: string = null;
@Output()
preview: EventEmitter<any> = new EventEmitter();
@ -102,10 +114,17 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
* Loads and displays search results
* @param searchTerm Search query entered by user
*/
public displaySearchResults(searchTerm): void {
if (searchTerm !== null && this.alfrescoSearchService !== null) {
private displaySearchResults(searchTerm): void {
if (searchTerm && this.alfrescoSearchService) {
let searchOpts: SearchOptions = {
include: ['path'],
rootNodeId: this.rootNodeId,
nodeType: this.resultType,
maxItems: this.maxResults,
orderBy: this.resultSort
};
this.alfrescoSearchService
.getLiveSearchResults(searchTerm)
.getNodeQueryResults(searchTerm, searchOpts)
.subscribe(
results => {
this.results = results.list.entries;

View File

@ -40,23 +40,34 @@ describe('AlfrescoSearchService', () => {
spyOn(authenticationService, 'getAlfrescoApi').and.returnValue(fakeApi);
});
it('should call search API with the correct parameters', (done) => {
it('should call search API with no additional options', (done) => {
let searchTerm = 'searchTerm63688';
spyOn(fakeApi.core.queriesApi, 'findNodes').and.returnValue(Promise.resolve(fakeSearch));
service.getNodeQueryResults(searchTerm).subscribe(
() => {
expect(fakeApi.core.queriesApi.findNodes).toHaveBeenCalledWith(searchTerm, undefined);
done();
}
);
});
it('should call search API with additional options', (done) => {
let searchTerm = 'searchTerm63688', options = {
include: [ 'path' ],
rootNodeId: '-root-',
nodeType: 'cm:content'
};
spyOn(fakeApi.core.searchApi, 'liveSearchNodes').and.returnValue(Promise.resolve(fakeSearch));
service.getLiveSearchResults(searchTerm).subscribe(
spyOn(fakeApi.core.queriesApi, 'findNodes').and.returnValue(Promise.resolve(fakeSearch));
service.getNodeQueryResults(searchTerm, options).subscribe(
() => {
expect(fakeApi.core.searchApi.liveSearchNodes).toHaveBeenCalledWith(searchTerm, options);
expect(fakeApi.core.queriesApi.findNodes).toHaveBeenCalledWith(searchTerm, options);
done();
}
);
});
it('should return search results returned from the API', (done) => {
service.getLiveSearchResults('').subscribe(
service.getNodeQueryResults('').subscribe(
(res: any) => {
expect(res).toBeDefined();
expect(res).toEqual(fakeSearch);
@ -66,8 +77,8 @@ describe('AlfrescoSearchService', () => {
});
it('should notify errors returned from the API', (done) => {
spyOn(fakeApi.core.searchApi, 'liveSearchNodes').and.returnValue(Promise.reject(fakeError));
service.getLiveSearchResults('').subscribe(
spyOn(fakeApi.core.queriesApi, 'findNodes').and.returnValue(Promise.reject(fakeError));
service.getNodeQueryResults('').subscribe(
() => {},
(res: any) => {
expect(res).toBeDefined();
@ -78,8 +89,8 @@ describe('AlfrescoSearchService', () => {
});
it('should notify a general error if the API does not return a specific error', (done) => {
spyOn(fakeApi.core.searchApi, 'liveSearchNodes').and.returnValue(Promise.reject(null));
service.getLiveSearchResults('').subscribe(
spyOn(fakeApi.core.queriesApi, 'findNodes').and.returnValue(Promise.reject(null));
service.getNodeQueryResults('').subscribe(
() => {},
(res: any) => {
expect(res).toBeDefined();

View File

@ -32,25 +32,30 @@ export class AlfrescoSearchService {
* Execute a search against the repository
*
* @param term Search term
* @param options Additional options passed to the search
* @returns {Observable<NodePaging>} Search results
*/
public getLiveSearchResults(term: string): Observable<any> {
return Observable.fromPromise(this.getSearchNodesPromise(term))
public getNodeQueryResults(term: string, options?: SearchOptions): Observable<any> {
return Observable.fromPromise(this.getQueryNodesPromise(term, options))
.map(res => <any> res)
.catch(this.handleError);
}
private getSearchNodesPromise(term: string) {
let nodeId = '-root-';
let opts = {
include: ['path'],
rootNodeId: nodeId,
nodeType: 'cm:content'
};
return this.authService.getAlfrescoApi().core.searchApi.liveSearchNodes(term, opts);
private getQueryNodesPromise(term: string, opts: SearchOptions) {
return this.authService.getAlfrescoApi().core.queriesApi.findNodes(term, opts);
}
private handleError(error: any): Observable<any> {
return Observable.throw(error || 'Server error');
}
}
export interface SearchOptions {
skipCount?: number;
maxItems?: number;
rootNodeId?: string;
nodeType?: string;
include?: string[];
orderBy?: string;
fields?: string[];
}