Allow navigation to folders from search results (#1209)

* Allow navigation to folders from search results

- Uses router to pass ID of the folder
- Modified document list component to accept folder ID without path
- Current limitations
  - Breadcrumb cannot currently be shown when navigating via folder id
  - Clicking between folders does not update the current route

* Allow root folder ID to be changed and have documentlist reload

- e.g switching from Company home to My Files

* New tests for navigating to folders based on ID

Refs #666
This commit is contained in:
Will Abson
2016-12-13 09:30:58 +00:00
committed by Denys Vuika
parent a8ef1f8e4e
commit b34a38fcff
21 changed files with 370 additions and 151 deletions

View File

@@ -225,7 +225,7 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| Name | Description |
| --- | --- |
| `preview` | Emitted when a file result is clicked/selected |
| `navigate` | Emitted when a search result is clicked or double-clicked |
| `resultsLoad` | Emitted when search results have fully loaded |
#### Options
@@ -237,6 +237,7 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| `resultType` | {boolean} | (optional) | (none) | Node type to filter search results by, e.g. 'cm:content'. |
| `maxResults` | {boolean} | (optional) | 20 | Maximum number of results to show in the search. |
| `resultSort` | {boolean} | (optional) | (none) | Criteria to sort search results by, must be one of "name" , "modifiedAt" or "createdAt" |
| `navigationMode` | {string} | (optional) | "dblclick" | Event used to initiate a navigation action to a specific result, one of "click" or "dblclick" |
### Build from sources

View File

@@ -214,7 +214,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
});
it('should emit file select when file item clicked', (done) => {
it('should emit fileSelect event when file item clicked', (done) => {
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
@@ -231,7 +231,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
});
});
it('should not emit preview if a non-file item is clicked', (done) => {
it('should emit fileSelect event if when folder item clicked', (done) => {
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
@@ -240,7 +240,7 @@ describe('AlfrescoSearchAutocompleteComponent', () => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<any> element.querySelector('#result_row_0')).click();
expect(component.fileSelect.emit).not.toHaveBeenCalled();
expect(component.fileSelect.emit).toHaveBeenCalled();
done();
});

View File

@@ -19,6 +19,7 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, OnChanges, Output,
import { AlfrescoSearchService, SearchOptions } from './../services/alfresco-search.service';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
@Component({
moduleId: module.id,
@@ -121,10 +122,12 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
* @param node Node to get URL for.
* @returns {string} URL address.
*/
getMimeTypeIcon(node: any): string {
getMimeTypeIcon(node: MinimalNodeEntity): string {
if (node.entry.content && node.entry.content.mimeType) {
let icon = this.alfrescoThumbnailService.getMimeTypeIcon(node.entry.content.mimeType);
return this.resolveIconPath(icon);
} else if (node.entry.isFolder) {
return 'ft_ic_folder.svg';
}
}
@@ -148,7 +151,7 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
* @param node Node to get URL for.
* @returns {string} URL address.
*/
getMimeTypeKey(node: any): string {
getMimeTypeKey(node: MinimalNodeEntity): string {
if (node.entry.content && node.entry.content.mimeType) {
return 'SEARCH.ICONS.' + this.alfrescoThumbnailService.getMimeTypeKey(node.entry.content.mimeType);
} else {
@@ -161,13 +164,9 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
firstResult.focus();
}
onItemClick(node): void {
onItemClick(node: MinimalNodeEntity): void {
if (node && node.entry) {
if (node.entry.isFile) {
this.fileSelect.emit({
value: node
});
}
this.fileSelect.emit(node);
}
}
@@ -179,12 +178,10 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
this.searchFocus.emit($event);
}
onRowEnter(node): void {
onRowEnter(node: MinimalNodeEntity): void {
if (node && node.entry) {
if (node.entry.isFile) {
this.fileSelect.emit({
value: node
});
this.fileSelect.emit(node);
}
}
}

View File

@@ -72,7 +72,7 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
liveSearchRoot: string = '-root-';
@Input()
liveSearchResultType: string = 'cm:content';
liveSearchResultType: string = null;
@Input()
liveSearchResultSort: string = null;
@@ -171,9 +171,7 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy {
}
onFileClicked(event): void {
this.fileSelect.emit({
value: event.value
});
this.fileSelect.emit(event);
}
onSearchFocus($event): void {

View File

@@ -16,7 +16,7 @@
</thead>
<tbody>
<tr id="result_row_{{idx}}" tabindex="0" *ngFor="let result of results; let idx = index" (click)="onItemClick(result, $event)" (keyup.enter)="onItemClick(result, $event)">
<tr id="result_row_{{idx}}" tabindex="0" *ngFor="let result of results; let idx = index" (click)="onItemClick(result, $event)" (dblclick)="onItemDblClick(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

@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { ReflectiveInjector, SimpleChange } from '@angular/core';
import { DebugElement, ReflectiveInjector, SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
@@ -256,44 +257,91 @@ describe('AlfrescoSearchComponent', () => {
});
});
describe('search result actions', () => {
describe('search result interactions', () => {
it('should emit preview when file item clicked', (done) => {
let debugElement: DebugElement;
let searchService: AlfrescoSearchService;
let querySpy: jasmine.Spy;
let emitSpy: jasmine.Spy;
const rowSelector = '[data-automation-id="search_result_table"] tbody tr';
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(result));
beforeEach(() => {
debugElement = fixture.debugElement;
searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
querySpy = spyOn(searchService, 'getQueryNodesPromise').and.returnValue(Promise.resolve(result));
emitSpy = spyOn(component.navigate, 'emit');
});
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<HTMLTableRowElement> element.querySelector('#result_row_0')).click();
describe('click results', () => {
beforeEach(() => {
component.navigationMode = AlfrescoSearchComponent.SINGLE_CLICK_NAVIGATION;
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
it('should emit navigation event when file item clicked', (done) => {
component.preview.subscribe(() => {
done();
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
debugElement.query(By.css(rowSelector)).triggerEventHandler('click', {});
expect(emitSpy).toHaveBeenCalled();
done();
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
});
it('should emit navigation event when non-file item is clicked', (done) => {
querySpy.and.returnValue(Promise.resolve(folderResult));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
debugElement.query(By.css(rowSelector)).triggerEventHandler('click', {});
expect(emitSpy).toHaveBeenCalled();
done();
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
});
});
it('should not emit preview when non-file item is clicked', (done) => {
describe('double click results', () => {
let searchService = fixture.debugElement.injector.get(AlfrescoSearchService);
spyOn(searchService, 'getQueryNodesPromise')
.and.returnValue(Promise.resolve(folderResult));
spyOn(component.preview, 'emit');
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<HTMLTableRowElement> element.querySelector('#result_row_0')).click();
expect(component.preview.emit).not.toHaveBeenCalled();
done();
beforeEach(() => {
component.navigationMode = AlfrescoSearchComponent.DOUBLE_CLICK_NAVIGATION;
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
it('should emit navigation event when file item clicked', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
debugElement.query(By.css(rowSelector)).triggerEventHandler('dblclick', {});
expect(emitSpy).toHaveBeenCalled();
done();
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
});
it('should emit navigation event when non-file item is clicked', (done) => {
querySpy.and.returnValue(Promise.resolve(folderResult));
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
debugElement.query(By.css(rowSelector)).triggerEventHandler('dblclick', {});
expect(emitSpy).toHaveBeenCalled();
done();
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
});
});
});
});

View File

@@ -20,6 +20,7 @@ import { ActivatedRoute, Params } from '@angular/router';
import { AlfrescoSearchService, SearchOptions } from './../services/alfresco-search.service';
import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
@Component({
moduleId: module.id,
@@ -29,6 +30,9 @@ import { AlfrescoTranslationService } from 'ng2-alfresco-core';
})
export class AlfrescoSearchComponent implements OnChanges, OnInit {
static SINGLE_CLICK_NAVIGATION: string = 'click';
static DOUBLE_CLICK_NAVIGATION: string = 'dblclick';
@Input()
searchTerm: string = '';
@@ -44,8 +48,11 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
@Input()
resultType: string = null;
@Input()
navigationMode: string = AlfrescoSearchComponent.DOUBLE_CLICK_NAVIGATION; // click|dblclick
@Output()
preview: EventEmitter<any> = new EventEmitter();
navigate: EventEmitter<MinimalNodeEntity> = new EventEmitter<MinimalNodeEntity>();
@Output()
resultsLoad = new EventEmitter();
@@ -92,6 +99,8 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
if (node.entry.content && node.entry.content.mimeType) {
let icon = this._alfrescoThumbnailService.getMimeTypeIcon(node.entry.content.mimeType);
return this.resolveIconPath(icon);
} else if (node.entry.isFolder) {
return 'ft_ic_folder.svg';
}
}
@@ -154,14 +163,17 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
}
onItemClick(node, event?: Event): void {
if (event) {
event.preventDefault();
if (this.navigate && this.navigationMode === AlfrescoSearchComponent.SINGLE_CLICK_NAVIGATION) {
if (node && node.entry) {
this.navigate.emit(node);
}
}
if (node && node.entry) {
if (node.entry.isFile) {
this.preview.emit({
value: node
});
}
onItemDblClick(node: MinimalNodeEntity) {
if (this.navigate && this.navigationMode === AlfrescoSearchComponent.DOUBLE_CLICK_NAVIGATION) {
if (node && node.entry) {
this.navigate.emit(node);
}
}
}