Merge pull request #927 from Alfresco/dev-wabson-371

Tab into search results
This commit is contained in:
Denys Vuika 2016-10-31 18:30:36 +00:00 committed by GitHub
commit 0107865072
13 changed files with 687 additions and 259 deletions

View File

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

View File

@ -92,7 +92,8 @@ Also make sure you include these dependencies in your .html page:
<alfresco-search-control [searchTerm]="searchTerm"
inputType="search"
(searchChange)="onSearchChange($event);"
(searchSubmit)="onSearchSubmit($event);">
(searchSubmit)="onSearchSubmit($event);"
(fileSelect)="onSearchResultSelect($event);">
</alfresco-search-control>
```
@ -172,6 +173,8 @@ 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
**expand**: Emitted when the expanded state of the control changes based on focus events and the content of the input control
#### Options

View File

@ -0,0 +1,88 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
const entryItem = {
entry: {
id: '123',
name: 'MyDoc',
isFile : true,
content: {
mimeType: 'text/plain'
},
createdByUser: {
displayName: 'John Doe'
},
modifiedByUser: {
displayName: 'John Doe'
}
}
};
export var result = {
list: {
entries: [
entryItem
]
}
};
export var results = {
list: {
entries: [
entryItem,
entryItem,
entryItem
]
}
};
export var folderResult = {
list: {
entries: [
{
entry: {
id: '123',
name: 'MyFolder',
isFile : false,
isFolder : true,
createdByUser: {
displayName: 'John Doe'
},
modifiedByUser: {
displayName: 'John Doe'
}
}
}
]
}
};
export var noResult = {
list: {
entries: []
}
};
export var errorJson = {
error: {
errorKey: 'Search failed',
statusCode: 400,
briefSummary: '08220082 search failed',
stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
descriptionURL: 'https://api-explorer.alfresco.com'
}
};

View File

@ -1,8 +1,14 @@
<table data-automation-id="autocomplete_results" *ngIf="results && results.length && searchTerm"
class="mdl-data-table mdl-js-data-table mdl-shadow--2dp full-width">
<tbody>
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" (click)="onItemClick(result, $event)"
attr.data-automation-id="autocomplete_result_for_{{result.entry.name}}">
<tbody #resultsTableBody>
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" tabindex="0"
(blur)="onRowBlur($event)" (focus)="onRowFocus($event)"
(click)="onItemClick(result)"
(keyup.enter)="onRowEnter(result)"
(keyup.arrowdown)="onRowArrowDown($event)"
(keyup.arrowup)="onRowArrowUp($event)"
(keyup.escape)="onRowEscape($event)"
attr.data-automation-id="autocomplete_result_for_{{result.entry.name}}">
<td class="img-td"><img src="{{getMimeTypeIcon(result)}}" alt="{{getMimeTypeKey(result)|translate}}"/></td>
<td>
<div id="result_name_{{idx}}" class="truncate"><b>{{result.entry.name}}</b></div>
@ -29,5 +35,3 @@
</tr>
</tbody>
</table>
<p data-automation-id="autocomplete_error_message" *ngIf="errorMessage">{{ 'SEARCH.RESULTS.ERROR' |
translate:{errorMessage: errorMessage} }}</p>

View File

@ -19,6 +19,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { TranslationMock } from './../assets/translation.service.mock';
import { result, results, folderResult, noResult, errorJson } from './../assets/alfresco-search.component.mock';
import { AlfrescoSearchService } from '../services/alfresco-search.service';
import {
AlfrescoApiService,
@ -34,64 +35,10 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
let fixture: ComponentFixture<AlfrescoSearchAutocompleteComponent>, element: HTMLElement;
let component: AlfrescoSearchAutocompleteComponent;
let result = {
list: {
entries: [
{
entry: {
id: '123',
name: 'MyDoc',
isFile : true,
content: {
mimeType: 'text/plain'
},
createdByUser: {
displayName: 'John Doe'
},
modifiedByUser: {
displayName: 'John Doe'
}
}
}
]
}
};
let folderResult = {
list: {
entries: [
{
entry: {
id: '123',
name: 'MyFolder',
isFile : false,
isFolder : true,
createdByUser: {
displayName: 'John Doe'
},
modifiedByUser: {
displayName: 'John Doe'
}
}
}
]
}
};
let noResult = {
list: {
entries: []
}
};
let errorJson = {
error: {
errorKey: 'Search failed',
statusCode: 400,
briefSummary: '08220082 search failed',
stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
descriptionURL: 'https://api-explorer.alfresco.com'
}
let updateSearchTerm = (newSearchTerm: string): void => {
let oldSearchTerm = component.searchTerm;
component.searchTerm = newSearchTerm;
component.ngOnChanges({searchTerm: { currentValue: newSearchTerm, previousValue: oldSearchTerm}});
};
beforeEach(async(() => {
@ -123,167 +70,352 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
expect(translationService.addTranslationFolder).toHaveBeenCalledWith('node_modules/ng2-alfresco-search/dist/src');
});
it('should display search results when a search term is provided', () => {
let searchTerm = { currentValue: 'customSearchTerm', previousValue: ''};
spyOn(component, 'displaySearchResults').and.stub();
component.searchTerm = 'searchTerm';
component.ngOnChanges({
searchTerm: searchTerm
describe('search results', () => {
let searchService;
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
});
fixture.detectChanges();
expect(component.displaySearchResults).toHaveBeenCalledWith(searchTerm.currentValue);
it('should clear results straight away when a new search term is entered', async(() => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} });
fixture.whenStable().then(() => {
fixture.detectChanges();
component.searchTerm = 'searchTerm2';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
fixture.detectChanges();
expect(element.querySelectorAll('table[data-automation-id="autocomplete_results"] tbody tr').length).toBe(0);
});
}));
it('should display the returned search results', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect( element.querySelector('#result_user_0').innerHTML).toBe('John Doe');
expect( element.querySelector('#result_name_0').innerHTML).toBe('<b _ngcontent-a-1="">MyDoc</b>');
done();
});
updateSearchTerm('searchTerm');
});
it('should limit the number of returned search results to the configured maximum', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(results));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(element.querySelectorAll('table[data-automation-id="autocomplete_results"] tbody tr').length).toBe(2);
done();
});
component.maxResults = 2;
updateSearchTerm('searchTerm');
});
it('should display the correct thumbnail for result items', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.baseComponentPath = 'http://localhost';
let thumbnailService = fixture.debugElement.injector.get(AlfrescoThumbnailService);
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('fake-type-icon.svg');
spyOn(thumbnailService, 'getMimeTypeKey').and.returnValue('FAKE_TYPE');
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let imgEl = <any> element.querySelector('#result_row_0 img');
expect(imgEl).not.toBeNull();
expect(imgEl.src).toBe('http://localhost/img/fake-type-icon.svg');
expect(imgEl.alt).toBe('SEARCH.ICONS.FAKE_TYPE');
done();
});
updateSearchTerm('searchTerm');
});
it('should display no result if no result are returned', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(noResult));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(element.querySelector('#search_no_result')).not.toBeNull();
done();
});
updateSearchTerm('searchTerm');
});
});
it('should clear results straight away when a new search term is entered', async(() => {
describe('errors', () => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
let searchService;
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} });
fixture.whenStable().then(() => {
fixture.detectChanges();
component.searchTerm = 'searchTerm2';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
fixture.detectChanges();
expect(element.querySelectorAll('table[data-automation-id="autocomplete_results"] tbody tr').length).toBe(0);
});
}));
it('should display the returned search results', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsEmitter.subscribe(x => {
fixture.detectChanges();
expect( element.querySelector('#result_user_0').innerHTML).toBe('John Doe');
expect( element.querySelector('#result_name_0').innerHTML).toBe('<b _ngcontent-a-1="">MyDoc</b>');
done();
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.reject(errorJson));
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} });
it('should display an error if an error is encountered running the search', (done) => {
component.resultsLoad.subscribe(() => {}, () => {
fixture.detectChanges();
let resultsEl = element.querySelector('[data-automation-id="autocomplete_results"]');
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
expect(resultsEl).toBeNull();
expect(errorEl).not.toBeNull();
expect(errorEl.innerText.trim()).toBe('SEARCH.RESULTS.ERROR');
done();
});
updateSearchTerm('searchTerm');
});
it('should clear errors straight away when a new search is performed', async(() => {
updateSearchTerm('searchTerm');
fixture.whenStable().then(() => {
fixture.detectChanges();
component.searchTerm = 'searchTerm2';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
fixture.detectChanges();
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
expect(errorEl).toBeNull();
});
}));
});
it('should display the correct thumbnail for result items', (done) => {
describe('mouse interactions', () => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
let searchService;
component.baseComponentPath = 'http://localhost';
let thumbnailService = fixture.debugElement.injector.get(AlfrescoThumbnailService);
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('fake-type-icon.svg');
spyOn(thumbnailService, 'getMimeTypeKey').and.returnValue('FAKE_TYPE');
component.resultsEmitter.subscribe(() => {
fixture.detectChanges();
let imgEl = <any> element.querySelector('#result_row_0 img');
expect(imgEl).not.toBeNull();
expect(imgEl.src).toBe('http://localhost/img/fake-type-icon.svg');
expect(imgEl.alt).toBe('SEARCH.ICONS.FAKE_TYPE');
done();
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
});
it('should emit file select when file item clicked', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).click();
});
updateSearchTerm('searchTerm');
component.fileSelect.subscribe(() => {
done();
});
});
it('should not emit preview if a non-file item is clicked', (done) => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.fileSelect, 'emit');
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).click();
expect(component.fileSelect.emit).not.toHaveBeenCalled();
done();
});
updateSearchTerm('searchTerm');
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} });
});
it('should display no result if no result are returned', (done) => {
describe('keyboard interactions', () => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(noResult));
let searchService;
component.resultsEmitter.subscribe(x => {
fixture.detectChanges();
expect(element.querySelector('#search_no_result')).not.toBeNull();
done();
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(results));
});
it('should emit file select when enter key pressed when a file item is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', {
key: 'Enter'
}));
});
updateSearchTerm('searchTerm');
component.fileSelect.subscribe(() => {
done();
});
});
it('should emit cancel event when escape key pressed when a result is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', {
key: 'Escape'
}));
});
updateSearchTerm('searchTerm');
component.cancel.subscribe(() => {
done();
});
});
it('should focus the next result when down arrow key pressed when a result is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let firstResult: any = element.querySelector('#result_row_0');
let secondResult: any = element.querySelector('#result_row_1');
spyOn(secondResult, 'focus');
firstResult.focus();
firstResult.dispatchEvent(new KeyboardEvent('keyup', {
key: 'ArrowDown'
}));
expect(secondResult.focus).toHaveBeenCalled();
done();
});
updateSearchTerm('searchTerm');
});
it('should do nothing when down arrow key pressed when the last result is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let lastResult: any = element.querySelector('#result_row_2');
lastResult.focus();
lastResult.dispatchEvent(new KeyboardEvent('keyup', {
key: 'ArrowDown'
}));
done();
});
updateSearchTerm('searchTerm');
});
it('should focus the previous result when up arrow key pressed when a result is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let firstResult: any = element.querySelector('#result_row_0');
let secondResult: any = element.querySelector('#result_row_1');
spyOn(firstResult, 'focus');
secondResult.focus();
secondResult.dispatchEvent(new KeyboardEvent('keyup', {
key: 'ArrowUp'
}));
expect(firstResult.focus).toHaveBeenCalled();
done();
});
updateSearchTerm('searchTerm');
});
it('should emit scroll back event when up arrow key pressed and the first result is in focus', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let firstResult: any = element.querySelector('#result_row_0');
firstResult.dispatchEvent(new KeyboardEvent('keyup', {
key: 'ArrowUp'
}));
});
component.scrollBack.subscribe(() => {
done();
});
updateSearchTerm('searchTerm');
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}});
});
it('should display an error if an error is encountered running the search', (done) => {
describe('changing focus', () => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.reject(errorJson));
let searchService;
component.errorEmitter.subscribe(() => {
fixture.detectChanges();
let resultsEl = element.querySelector('[data-automation-id="autocomplete_results"]');
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
expect(resultsEl).toBeNull();
expect(errorEl).not.toBeNull();
expect(errorEl.innerText.trim()).toBe('SEARCH.RESULTS.ERROR');
done();
beforeEach(() => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}});
});
it('should emit a focus event when a result comes into focus', (done) => {
it('should clear errors straight away when a new search is performed', async(() => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('focus'));
});
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.reject(errorJson));
updateSearchTerm('searchTerm');
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}});
fixture.whenStable().then(() => {
fixture.detectChanges();
component.searchTerm = 'searchTerm2';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
fixture.detectChanges();
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
expect(errorEl).toBeNull();
});
}));
it('should emit preview when file item clicked', (done) => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsEmitter.subscribe(x => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).click();
component.searchFocus.subscribe((e: FocusEvent) => {
expect(e).not.toBeNull();
expect(e.type).toBe('focus');
done();
});
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}});
it('should emit a focus event when a result loses focus', (done) => {
component.preview.subscribe(e => {
done();
});
});
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('blur'));
});
it('should not emit preview if a non-file item is clicked', (done) => {
updateSearchTerm('searchTerm');
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.preview, 'emit');
component.resultsEmitter.subscribe(x => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).click();
expect(component.preview.emit).not.toHaveBeenCalled();
done();
component.searchFocus.subscribe((e: FocusEvent) => {
expect(e).not.toBeNull();
expect(e.type).toBe('blur');
done();
});
});
it('should give focus to the first result when focusResult() is called externally', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
let firstResult: any = element.querySelector('#result_row_0');
spyOn(firstResult, 'focus');
component.focusResult();
expect(firstResult.focus).toHaveBeenCalled();
done();
});
updateSearchTerm('searchTerm');
});
component.searchTerm = 'searchTerm';
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}});
});
});

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnInit, OnChanges, Output } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnInit, OnChanges, Output, ViewChild } from '@angular/core';
import { AlfrescoSearchService } from './../services/alfresco-search.service';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
@ -40,14 +40,25 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
@Input()
ngClass: any;
@Output()
preview: EventEmitter<any> = new EventEmitter();
@Input()
maxResults: number = 5;
@Output()
resultsEmitter = new EventEmitter();
fileSelect: EventEmitter<any> = new EventEmitter();
@Output()
errorEmitter = new EventEmitter();
searchFocus: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
@Output()
cancel = new EventEmitter();
@Output()
resultsLoad = new EventEmitter();
@Output()
scrollBack = new EventEmitter();
@ViewChild('resultsTableBody', {}) resultsTableBody: ElementRef;
constructor(private alfrescoSearchService: AlfrescoSearchService,
private translate: AlfrescoTranslationService,
@ -72,20 +83,20 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
* Loads and displays search results
* @param searchTerm Search query entered by user
*/
public displaySearchResults(searchTerm) {
private displaySearchResults(searchTerm) {
if (searchTerm !== null && searchTerm !== '') {
this.alfrescoSearchService
.getLiveSearchResults(searchTerm)
.subscribe(
results => {
this.results = results.list.entries;
this.results = results.list.entries.slice(0, this.maxResults);
this.errorMessage = null;
this.resultsEmitter.emit(this.results);
this.resultsLoad.emit(this.results);
},
error => {
this.results = null;
this.errorMessage = <any>error;
this.errorEmitter.emit(error);
this.resultsLoad.error(error);
}
);
}
@ -116,17 +127,65 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
}
}
onItemClick(node, event?: Event): void {
if (event) {
event.preventDefault();
}
focusResult(): void {
let firstResult: any = this.resultsTableBody.nativeElement.querySelector('tr');
firstResult.focus();
}
onItemClick(node): void {
if (node && node.entry) {
if (node.entry.isFile) {
this.preview.emit({
this.fileSelect.emit({
value: node
});
}
}
}
onRowFocus($event: FocusEvent): void {
this.searchFocus.emit($event);
}
onRowBlur($event: FocusEvent): void {
this.searchFocus.emit($event);
}
onRowEnter(node): void {
if (node && node.entry) {
if (node.entry.isFile) {
this.fileSelect.emit({
value: node
});
}
}
}
private getNextElementSibling(node: Element): Element {
return node.nextElementSibling;
}
private getPreviousElementSibling(node: Element): Element {
return node.previousElementSibling;
}
onRowArrowDown($event: KeyboardEvent): void {
let nextElement: any = this.getNextElementSibling(<Element> $event.target);
if (nextElement) {
nextElement.focus();
}
}
onRowArrowUp($event: KeyboardEvent): void {
let previousElement: any = this.getPreviousElementSibling(<Element> $event.target);
if (previousElement) {
previousElement.focus();
} else {
this.scrollBack.emit($event);
}
}
onRowEscape($event: KeyboardEvent): void {
this.cancel.emit($event);
}
}

View File

@ -22,6 +22,9 @@
</div>
</div>
</form>
<alfresco-search-autocomplete *ngIf="autocompleteEnabled"
<alfresco-search-autocomplete #autocomplete *ngIf="autocompleteEnabled"
[searchTerm]="autocompleteSearchTerm" [ngClass]="{active: searchActive, valid: searchValid}"
(preview)="onFileClicked($event)"></alfresco-search-autocomplete>
(fileSelect)="onFileClicked($event)"
(searchFocus)="onAutoCompleteFocus($event)"
(scrollBack)="onAutoCompleteReturn($event)"
(cancel)="onAutoCompleteCancel($event)"></alfresco-search-autocomplete>

View File

@ -20,6 +20,7 @@ import { AlfrescoSearchControlComponent } from './alfresco-search-control.compon
import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { TranslationMock } from './../assets/translation.service.mock';
import { result } from './../assets/alfresco-search.component.mock';
import {
AlfrescoSettingsService,
AlfrescoApiService,
@ -150,24 +151,45 @@ describe('AlfrescoSearchControlComponent', () => {
expect(autocomplete.classList.contains('active')).toBe(false);
});
it('should make find-as-you-type control visible when search box has focus', () => {
it('should make find-as-you-type control visible when search box has focus', (done) => {
fixture.detectChanges();
inputEl.dispatchEvent(new Event('focus'));
fixture.detectChanges();
let autocomplete: Element = element.querySelector('alfresco-search-autocomplete');
expect(autocomplete.classList.contains('active')).toBe(true);
inputEl.dispatchEvent(new FocusEvent('focus'));
window.setTimeout(() => { // wait for debounce() to complete
fixture.detectChanges();
let autocomplete: Element = element.querySelector('alfresco-search-autocomplete');
expect(autocomplete.classList.contains('active')).toBe(true);
done();
}, 100);
});
it('should hide find-as-you-type results when the search box loses focus', (done) => {
fixture.detectChanges();
inputEl.dispatchEvent(new Event('focus'));
inputEl.dispatchEvent(new Event('blur'));
inputEl.dispatchEvent(new FocusEvent('focus'));
inputEl.dispatchEvent(new FocusEvent('blur'));
window.setTimeout(() => {
fixture.detectChanges();
let autocomplete: Element = element.querySelector('alfresco-search-autocomplete');
expect(autocomplete.classList.contains('active')).toBe(false);
done();
}, 250);
}, 100);
});
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')
.and.returnValue(Promise.resolve(result));
fixture.detectChanges();
inputEl.dispatchEvent(new FocusEvent('focus'));
fixture.detectChanges();
inputEl.dispatchEvent(new FocusEvent('blur'));
component.onAutoCompleteFocus(new FocusEvent('focus'));
window.setTimeout(() => { // wait for debounce() to complete
fixture.detectChanges();
let autocomplete: Element = element.querySelector('alfresco-search-autocomplete');
expect(autocomplete.classList.contains('active')).toBe(true);
done();
}, 100);
});
it('should hide find-as-you-type results when escape key pressed', () => {
@ -195,6 +217,42 @@ describe('AlfrescoSearchControlComponent', () => {
expect(autocomplete.classList.contains('active')).toBe(true);
});
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');
fixture.detectChanges();
inputEl.dispatchEvent(new Event('focus'));
window.setTimeout(() => { // wait for debounce() to complete
fixture.detectChanges();
inputEl.dispatchEvent(new KeyboardEvent('keyup', {
key: 'ArrowDown'
}));
fixture.detectChanges();
expect(component.autocompleteComponent.focusResult).toHaveBeenCalled();
done();
}, 100);
});
it('should focus input element when find-as-you-type returns control', () => {
fixture.detectChanges();
spyOn(inputEl, 'focus');
fixture.detectChanges();
component.onAutoCompleteReturn(new KeyboardEvent('keyup', {
key: 'ArrowUp'
}));
expect(inputEl.focus).toHaveBeenCalled();
});
it('should focus input element when find-as-you-type is cancelled', () => {
fixture.detectChanges();
spyOn(inputEl, 'focus');
fixture.detectChanges();
component.onAutoCompleteCancel(new KeyboardEvent('keyup', {
key: 'ArrowUp'
}));
expect(inputEl.focus).toHaveBeenCalled();
});
it('should NOT display a find-as-you-type control when configured not to', () => {
fixture.componentInstance.autocompleteEnabled = false;
fixture.detectChanges();
@ -239,44 +297,53 @@ describe('AlfrescoSearchControlComponent', () => {
describe('component focus', () => {
it('should fire an event when the search box receives focus', () => {
it('should fire an event when the search box receives focus', (done) => {
spyOn(component.expand, 'emit');
let inputEl: HTMLElement = element.querySelector('input');
inputEl.dispatchEvent(new Event('focus'));
expect(component.expand.emit).toHaveBeenCalledWith({
expanded: true
});
inputEl.dispatchEvent(new FocusEvent('focus'));
window.setTimeout(() => {
expect(component.expand.emit).toHaveBeenCalledWith({
expanded: true
});
done();
}, 100);
});
it('should fire an event when the search box loses focus', () => {
it('should fire an event when the search box loses focus', (done) => {
spyOn(component.expand, 'emit');
let inputEl: HTMLElement = element.querySelector('input');
inputEl.dispatchEvent(new Event('blur'));
expect(component.expand.emit).toHaveBeenCalledWith({
expanded: false
});
inputEl.dispatchEvent(new FocusEvent('blur'));
window.setTimeout(() => {
expect(component.expand.emit).toHaveBeenCalledWith({
expanded: false
});
done();
}, 100);
});
it('should NOT fire an event when the search box receives/loses focus but the component is not expandable',
() => {
(done) => {
spyOn(component.expand, 'emit');
component.expandable = false;
let inputEl: HTMLElement = element.querySelector('input');
inputEl.dispatchEvent(new Event('focus'));
inputEl.dispatchEvent(new Event('blur'));
expect(component.expand.emit).not.toHaveBeenCalled();
inputEl.dispatchEvent(new FocusEvent('focus'));
inputEl.dispatchEvent(new FocusEvent('blur'));
window.setTimeout(() => {
expect(component.expand.emit).not.toHaveBeenCalled();
done();
}, 100);
});
});
describe('file preview', () => {
it('should emit a preview event when onFileClicked is called', () => {
spyOn(component.preview, 'emit');
it('should emit a file select event when onFileClicked is called', () => {
spyOn(component.fileSelect, 'emit');
component.onFileClicked({
value: 'node12345'
});
expect(component.preview.emit).toHaveBeenCalledWith({
expect(component.fileSelect.emit).toHaveBeenCalledWith({
'value': 'node12345'
});
});

View File

@ -16,9 +16,11 @@
*/
import { FormControl, Validators } from '@angular/forms';
import { Component, Input, Output, OnInit, ElementRef, EventEmitter, ViewChild } from '@angular/core';
import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ViewChild } from '@angular/core';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component';
import { SearchTermValidator } from './../forms/search-term-validator';
import { Observable, Subject } from 'rxjs/Rx';
@Component({
moduleId: module.id,
@ -26,7 +28,7 @@ import { SearchTermValidator } from './../forms/search-term-validator';
templateUrl: './alfresco-search-control.component.html',
styleUrls: ['./alfresco-search-control.component.css']
})
export class AlfrescoSearchControlComponent implements OnInit {
export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
@Input()
searchTerm = '';
@ -47,7 +49,7 @@ export class AlfrescoSearchControlComponent implements OnInit {
searchSubmit = new EventEmitter();
@Output()
preview = new EventEmitter();
fileSelect = new EventEmitter();
@Output()
expand = new EventEmitter();
@ -56,6 +58,9 @@ export class AlfrescoSearchControlComponent implements OnInit {
@ViewChild('searchInput', {}) searchInput: ElementRef;
@ViewChild('autocomplete')
autocompleteComponent: AlfrescoSearchAutocompleteComponent;
@Input()
autocompleteEnabled = true;
@ -66,6 +71,8 @@ export class AlfrescoSearchControlComponent implements OnInit {
searchValid = false;
private focusSubject = new Subject<FocusEvent>();
constructor(private translate: AlfrescoTranslationService) {
this.searchControl = new FormControl(
@ -80,11 +87,18 @@ export class AlfrescoSearchControlComponent implements OnInit {
this.onSearchTermChange(value);
}
);
this.setupFocusEventHandlers();
this.translate.addTranslationFolder('node_modules/ng2-alfresco-search/dist/src');
}
ngOnDestroy(): void {
this.focusSubject.unsubscribe();
}
private onSearchTermChange(value: string): void {
this.searchActive = true;
this.setAutoCompleteDisplayed(true);
this.autocompleteSearchTerm = value;
this.searchControl.setValue(value, true);
this.searchValid = this.searchControl.valid;
@ -94,6 +108,20 @@ export class AlfrescoSearchControlComponent implements OnInit {
});
}
private setupFocusEventHandlers() {
let focusEvents: Observable<FocusEvent> = this.focusSubject.asObservable().debounceTime(50);
focusEvents.filter(($event: FocusEvent) => {
return $event.type === 'focusin' || $event.type === 'focus';
}).subscribe(($event) => {
this.onSearchFocus($event);
});
focusEvents.filter(($event: any) => {
return $event.type === 'focusout' || $event.type === 'blur';
}).subscribe(($event) => {
this.onSearchBlur($event);
});
}
getTextFieldClassName(): string {
return 'mdl-textfield mdl-js-textfield' + (this.expandable ? ' mdl-textfield--expandable' : '');
}
@ -121,38 +149,73 @@ export class AlfrescoSearchControlComponent implements OnInit {
}
}
isAutoCompleteDisplayed(): boolean {
return this.searchActive;
}
setAutoCompleteDisplayed(display: boolean): void {
this.searchActive = display;
}
onFileClicked(event): void {
this.preview.emit({
this.fileSelect.emit({
value: event.value
});
}
onFocus(): void {
this.searchActive = true;
onSearchFocus($event): void {
this.setAutoCompleteDisplayed(true);
}
onSearchBlur($event): void {
this.setAutoCompleteDisplayed(false);
}
onFocus($event): void {
if (this.expandable) {
this.expand.emit({
expanded: true
});
}
this.focusSubject.next($event);
}
onBlur(): void {
window.setTimeout(() => {
this.searchActive = false;
}, 200);
onBlur($event): void {
if (this.expandable && (this.searchControl.value === '' || this.searchControl.value === undefined)) {
this.expand.emit({
expanded: false
});
}
this.focusSubject.next($event);
}
onEscape(): void {
this.searchActive = false;
this.setAutoCompleteDisplayed(false);
}
onArrowDown(): void {
this.searchActive = true;
if (this.isAutoCompleteDisplayed()) {
this.autocompleteComponent.focusResult();
} else {
this.setAutoCompleteDisplayed(true);
}
}
onAutoCompleteFocus($event): void {
this.focusSubject.next($event);
}
onAutoCompleteReturn($event): void {
if (this.searchInput) {
(<any> this.searchInput.nativeElement).focus();
}
}
onAutoCompleteCancel($event): void {
if (this.searchInput) {
(<any> this.searchInput.nativeElement).focus();
}
this.setAutoCompleteDisplayed(false);
}
}

View File

@ -16,7 +16,7 @@
</thead>
<tbody>
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" (click)="onItemClick(result, $event)">
<tr id="result_row_{{idx}}" tabindex="0" *ngFor="let result of results; let idx = index" (click)="onItemClick(result, $event)" (keyup.enter)="onItemClick(result, $event)">
<td class="col-mimetype-icon"><img src="{{getMimeTypeIcon(result)}}" alt="{{getMimeTypeKey(result)|translate}}" /></td>
<td id="result_name_{{idx}}" class="mdl-data-table__cell--non-numeric col-display-name"
attr.data-automation-id=file_{{result.entry.name}} >{{result.entry.name}}</td>

View File

@ -162,7 +162,7 @@ describe('AlfrescoSearchComponent', () => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsEmitter.subscribe(x => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(searchService.getSearchNodesPromise).toHaveBeenCalled();
expect(element.querySelector('#result_user_0')).not.toBeNull();
@ -181,7 +181,7 @@ describe('AlfrescoSearchComponent', () => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(noResult));
component.resultsEmitter.subscribe(x => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(element.querySelector('#search_no_result')).not.toBeNull();
done();
@ -197,7 +197,7 @@ describe('AlfrescoSearchComponent', () => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.reject(errorJson));
component.errorEmitter.subscribe(() => {
component.resultsLoad.subscribe(() => {}, () => {
fixture.detectChanges();
let resultsEl = element.querySelector('[data-automation-id="search_result_table"]');
let errorEl = element.querySelector('[data-automation-id="search_error_message"]');
@ -217,7 +217,7 @@ describe('AlfrescoSearchComponent', () => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsEmitter.subscribe(x => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
expect(searchService.getSearchNodesPromise).toHaveBeenCalledWith('searchTerm2');
expect(element.querySelector('#result_user_0')).not.toBeNull();
@ -238,7 +238,7 @@ describe('AlfrescoSearchComponent', () => {
spyOn(searchService, 'getSearchNodesPromise')
.and.returnValue(Promise.resolve(result));
component.resultsEmitter.subscribe(() => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<HTMLTableRowElement> element.querySelector('#result_row_0')).click();
});
@ -246,7 +246,7 @@ describe('AlfrescoSearchComponent', () => {
component.searchTerm = 'searchTerm';
component.ngOnInit();
component.preview.subscribe(e => {
component.preview.subscribe(() => {
done();
});
});
@ -258,7 +258,7 @@ describe('AlfrescoSearchComponent', () => {
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.preview, 'emit');
component.resultsEmitter.subscribe(x => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<HTMLTableRowElement> element.querySelector('#result_row_0')).click();
expect(component.preview.emit).not.toHaveBeenCalled();

View File

@ -38,10 +38,7 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
preview: EventEmitter<any> = new EventEmitter();
@Output()
resultsEmitter = new EventEmitter();
@Output()
errorEmitter = new EventEmitter();
resultsLoad = new EventEmitter();
results: any = null;
@ -112,13 +109,13 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
.subscribe(
results => {
this.results = results.list.entries;
this.resultsEmitter.emit(this.results);
this.resultsLoad.emit(this.results);
this.errorMessage = null;
},
error => {
this.results = null;
this.errorMessage = <any>error;
this.errorEmitter.emit(error);
this.resultsLoad.error(error);
}
);
}

View File

@ -77,4 +77,16 @@ 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(
() => {},
(res: any) => {
expect(res).toBeDefined();
expect(res).toEqual('Server error');
done();
}
);
});
});