Update search component to work with 0.4.0 JS API

- Use new search (query) methods
- Allow search parameters to be overridden by component
- Fix up tests

Refs #555
This commit is contained in:
Will Abson
2016-11-04 14:21:19 +00:00
parent 903c313615
commit 0e127c26af
13 changed files with 172 additions and 65 deletions

View File

@@ -180,9 +180,13 @@ bootstrap(SearchDemo, [
**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
@@ -337,6 +341,10 @@ None
**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[];
}