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

@@ -87,7 +87,7 @@ describe('DocumentListBreadcrumb', () => {
it('should update document list on click', (done) => {
let documentList = new DocumentList(null, null, null);
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.resolve());
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve());
let node = <PathNode> { name: 'name', path: '/path' };
component.target = documentList;

View File

@@ -83,8 +83,11 @@ export class DocumentListBreadcrumb {
}
});
this.currentFolderPath = route.path;
if (this.target) {
this.target.currentFolderPath = route.path;
this.target.loadFolder();
}
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { NgZone, TemplateRef } from '@angular/core';
import { NgZone, SimpleChange, TemplateRef } from '@angular/core';
import { DataTableComponent, DataColumn, DataRowEvent } from 'ng2-alfresco-datatable';
import { DocumentList } from './document-list';
import { DocumentListServiceMock } from './../assets/document-list.service.mock';
@@ -48,12 +48,12 @@ describe('DocumentList', () => {
window['componentHandler'] = componentHandler;
});
it('should update root path', () => {
it('should update root folder ID', () => {
let adapter = documentList.data;
expect(adapter.rootPath).toBe(adapter.DEFAULT_ROOT_PATH);
expect(adapter.rootFolderId).toBe(adapter.DEFAULT_ROOT_ID);
documentList.rootPath = '-shared-';
expect(adapter.rootPath).toBe('-shared-');
documentList.rootFolderId = '-shared-';
expect(adapter.rootFolderId).toBe('-shared-');
});
it('should setup default columns', () => {
@@ -163,7 +163,7 @@ describe('DocumentList', () => {
let node = new FolderNode('<display name>');
spyOn(documentList, 'getNodePath').and.returnValue(path);
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve(true));
documentList.navigationMode = DocumentList.SINGLE_CLICK_NAVIGATION;
documentList.onNodeClick(node);
@@ -173,31 +173,31 @@ describe('DocumentList', () => {
it('should not display folder content when no target node provided', () => {
expect(documentList.navigate).toBe(true);
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.stub();
documentList.onNodeClick(null);
expect(documentList.displayFolderContent).not.toHaveBeenCalled();
expect(documentList.loadFolderByPath).not.toHaveBeenCalled();
});
it('should display folder content only on folder node click', () => {
expect(documentList.navigate).toBe(true);
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.stub();
let node = new FileNode();
documentList.onNodeClick(node);
expect(documentList.displayFolderContent).not.toHaveBeenCalled();
expect(documentList.loadFolderByPath).not.toHaveBeenCalled();
});
it('should not display folder content on click when navigation is off', () => {
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.stub();
let node = new FolderNode('<display name>');
documentList.navigate = false;
documentList.onNodeClick(node);
expect(documentList.displayFolderContent).not.toHaveBeenCalled();
expect(documentList.loadFolderByPath).not.toHaveBeenCalled();
});
it('should require node to get path', () => {
@@ -205,31 +205,35 @@ describe('DocumentList', () => {
});
it('should display folder content for new folder path', () => {
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.resolve());
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve());
let newPath = '/some/new/path';
documentList.currentFolderPath = newPath;
expect(documentList.displayFolderContent).toHaveBeenCalledWith(newPath);
documentList.ngOnChanges({currentFolderPath: new SimpleChange(null, newPath)});
expect(documentList.loadFolderByPath).toHaveBeenCalledWith(newPath);
});
it('should reset to default path', () => {
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.resolve());
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve());
documentList.currentFolderPath = null;
documentList.ngOnChanges({currentFolderPath: new SimpleChange('', null)});
expect(documentList.currentFolderPath).toBe(documentList.DEFAULT_ROOT_FOLDER);
expect(documentList.displayFolderContent).toHaveBeenCalledWith(documentList.DEFAULT_ROOT_FOLDER);
expect(documentList.currentFolderPath).toBe(documentList.DEFAULT_FOLDER_PATH);
expect(documentList.loadFolderByPath).toHaveBeenCalledWith(documentList.DEFAULT_FOLDER_PATH);
});
it('should emit folder changed event', (done) => {
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.resolve());
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve());
documentList.folderChange.subscribe(e => {
done();
});
documentList.currentFolderPath = '/some/new/path';
let newPath = '/some/new/path';
documentList.currentFolderPath = newPath;
documentList.ngOnChanges({currentFolderPath: new SimpleChange(null, newPath)});
});
it('should emit folder changed event with folder details', (done) => {
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.resolve());
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve());
let path = '/path';
@@ -239,6 +243,7 @@ describe('DocumentList', () => {
});
documentList.currentFolderPath = path;
documentList.ngOnChanges({currentFolderPath: new SimpleChange(null, path)});
});
it('should execute context action on callback', () => {
@@ -259,7 +264,7 @@ describe('DocumentList', () => {
});
it('should subscribe to context action handler', () => {
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.resolve(true));
spyOn(documentList, 'contextActionCallback').and.stub();
let value = {};
documentList.ngOnInit();
@@ -384,16 +389,16 @@ describe('DocumentList', () => {
});
it('should display folder content on reload', () => {
spyOn(documentList, 'displayFolderContent').and.callThrough();
spyOn(documentList, 'loadFolderByPath').and.callThrough();
documentList.reload();
expect(documentList.displayFolderContent).toHaveBeenCalled();
expect(documentList.loadFolderByPath).toHaveBeenCalled();
});
it('should require path to display folder content', () => {
spyOn(documentListService, 'getFolder').and.callThrough();
documentList.displayFolderContent(null);
documentList.displayFolderContent('');
documentList.loadFolderByPath(null);
documentList.loadFolderByPath('');
expect(documentListService.getFolder).not.toHaveBeenCalled();
});
@@ -464,11 +469,11 @@ describe('DocumentList', () => {
});
expect(documentList.currentFolderPath).toBeNull();
spyOn(documentList, 'displayFolderContent').and.stub();
spyOn(documentList, 'loadFolderByPath').and.stub();
documentList.reload();
expect(documentList.displayFolderContent).not.toHaveBeenCalled();
expect(documentList.loadFolderByPath).not.toHaveBeenCalled();
});
it('should enforce single-click on mobile browser', () => {
@@ -482,9 +487,10 @@ describe('DocumentList', () => {
it('should emit error on wrong path', (done) => {
let raised = false;
documentList.error.subscribe(err => raised = true);
spyOn(documentList, 'displayFolderContent').and.returnValue(Promise.reject(false));
spyOn(documentList, 'loadFolderByPath').and.returnValue(Promise.reject(false));
documentList.currentFolderPath = 'wrong-path';
documentList.ngOnChanges({currentFolderPath: new SimpleChange(null, documentList.currentFolderPath)});
setTimeout(() => {
expect(raised).toBeTruthy();
done();
@@ -506,24 +512,24 @@ describe('DocumentList', () => {
expect(documentList.isEmptyTemplateDefined()).toBeFalsy();
});
it('should set root path for underlying adapter', () => {
documentList.rootPath = 'test';
expect(documentList.data.rootPath).toBe('test');
it('should set root folder ID for underlying adapter', () => {
documentList.rootFolderId = 'test';
expect(documentList.data.rootFolderId).toBe('test');
});
it('should set default root path for underlying adapter', () => {
documentList.rootPath = null;
expect(documentList.data.rootPath).toBe(documentList.data.DEFAULT_ROOT_PATH);
it('should set default root folder ID for underlying adapter', () => {
documentList.rootFolderId = null;
expect(documentList.data.rootFolderId).toBe(documentList.data.DEFAULT_ROOT_ID);
});
it('should fetch root path from underlying adapter', () => {
documentList.data.rootPath = 'test';
expect(documentList.rootPath).toBe('test');
it('should fetch root folder ID from underlying adapter', () => {
documentList.data.rootFolderId = 'test';
expect(documentList.rootFolderId).toBe('test');
});
it('should not fetch root path when adapter missing', () => {
it('should not fetch root folder ID when adapter missing', () => {
documentList.data = null;
expect(documentList.rootPath).toBeNull();
expect(documentList.rootFolderId).toBeNull();
});
it('should set row filter for underlying adapter', () => {
@@ -561,4 +567,49 @@ describe('DocumentList', () => {
documentList.onRowDblClick(event);
expect(documentList.onNodeDblClick).toHaveBeenCalledWith(node);
});
describe('navigate by folder ID', () => {
it('should load folder by ID on init', () => {
documentList.currentFolderId = '1d26e465-dea3-42f3-b415-faa8364b9692';
let loadbyIdSpy: jasmine.Spy = spyOn(documentList.data, 'loadById').and.returnValue(Promise.resolve());
documentList.ngOnInit();
expect(loadbyIdSpy).toHaveBeenCalled();
expect(documentList.currentFolderPath).toBe('/');
});
it('should load folder by ID on changes', () => {
let newNodeId = '1d26e465-dea3-42f3-b415-faa8364b9692';
documentList.ngOnChanges({currentFolderId: new SimpleChange(null, newNodeId)});
let loadbyPathSpy: jasmine.Spy = spyOn(documentList.data, 'loadPath').and.returnValue(Promise.resolve());
documentList.ngOnInit();
expect(loadbyPathSpy).toHaveBeenCalled();
expect(documentList.currentFolderPath).toBe('/');
});
});
describe('configure root folder', () => {
it('should re-load folder when rootFolderId changed', () => {
let newRootFolder = '-new-';
documentList.ngOnChanges({rootFolderId: new SimpleChange(null, newRootFolder)});
let loadbyPathSpy: jasmine.Spy = spyOn(documentList.data, 'loadPath').and.returnValue(Promise.resolve());
documentList.ngOnInit();
expect(loadbyPathSpy).toHaveBeenCalled();
expect(documentList.currentFolderPath).toBe('/');
});
});
});

View File

@@ -19,7 +19,9 @@ import {
Component,
OnInit,
Input,
OnChanges,
Output,
SimpleChanges,
EventEmitter,
AfterContentInit,
TemplateRef,
@@ -48,22 +50,25 @@ declare var module: any;
styleUrls: ['./document-list.css'],
templateUrl: './document-list.html'
})
export class DocumentList implements OnInit, AfterContentInit {
export class DocumentList implements OnInit, OnChanges, AfterContentInit {
static SINGLE_CLICK_NAVIGATION: string = 'click';
static DOUBLE_CLICK_NAVIGATION: string = 'dblclick';
static DEFAULT_PAGE_SIZE: number = 20;
DEFAULT_ROOT_FOLDER: string = '/';
DEFAULT_FOLDER_PATH: string = '/';
@Input()
set rootPath(value: string) {
this.data.rootPath = value || this.data.DEFAULT_ROOT_PATH;
set rootFolderId(value: string) {
this.data.rootFolderId = value || this.data.DEFAULT_ROOT_ID;
}
get rootPath(): string {
@Input()
currentFolderId: string = null;
get rootFolderId(): string {
if (this.data) {
return this.data.rootPath;
return this.data.rootFolderId;
}
return null;
}
@@ -127,21 +132,11 @@ export class DocumentList implements OnInit, AfterContentInit {
@ViewChild(DataTableComponent)
dataTable: DataTableComponent;
private _path = this.DEFAULT_ROOT_FOLDER;
private _path = this.DEFAULT_FOLDER_PATH;
@Input()
set currentFolderPath(value: string) {
if (value !== this._path) {
const path = value || this.DEFAULT_ROOT_FOLDER;
this.displayFolderContent(path)
.then(() => {
this._path = path;
this.folderChange.emit({ path: path });
})
.catch(err => {
this.error.emit(err);
});
}
this._path = value;
}
get currentFolderPath(): string {
@@ -220,6 +215,8 @@ export class DocumentList implements OnInit, AfterContentInit {
if (this.isMobile()) {
this.navigationMode = DocumentList.SINGLE_CLICK_NAVIGATION;
}
this.loadFolder();
}
ngAfterContentInit() {
@@ -229,6 +226,40 @@ export class DocumentList implements OnInit, AfterContentInit {
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['currentFolderId'] && changes['currentFolderId'].currentValue) {
let folderId = changes['currentFolderId'].currentValue;
this.loadFolderById(folderId)
.then(() => {
this._path = null;
})
.catch(err => {
this.error.emit(err);
});
} else if (changes['currentFolderPath']) {
const path = changes['currentFolderPath'].currentValue || this.DEFAULT_FOLDER_PATH;
this.currentFolderPath = path;
this.loadFolderByPath(path)
.then(() => {
this._path = path;
this.folderChange.emit({ path: path });
})
.catch(err => {
this.error.emit(err);
});
} else if (changes['rootFolderId']) {
// this.currentFolderPath = this.DEFAULT_FOLDER_PATH;
this.loadFolderByPath(this.currentFolderPath)
.then(() => {
this._path = this.currentFolderPath;
this.folderChange.emit({ path: this.currentFolderPath });
})
.catch(err => {
this.error.emit(err);
});
}
}
isEmptyTemplateDefined() {
if (this.dataTable) {
if (this.emptyFolderTemplate) {
@@ -277,6 +308,9 @@ export class DocumentList implements OnInit, AfterContentInit {
performNavigation(node: MinimalNodeEntity): boolean {
if (node && node.entry && node.entry.isFolder) {
this.currentFolderPath = this.getNodePath(node);
this.currentFolderId = null;
this.loadFolder();
this.folderChange.emit({ path: this.currentFolderPath });
return true;
}
return false;
@@ -293,21 +327,34 @@ export class DocumentList implements OnInit, AfterContentInit {
}
}
displayFolderContent(path: string): Promise<any> {
loadFolderByPath(path: string): Promise<any> {
return this.data.loadPath(path);
}
loadFolderById(id: string): Promise<any> {
return this.data.loadById(id);
}
reload() {
this.ngZone.run(() => {
if (this.currentFolderPath) {
this.displayFolderContent(this.currentFolderPath)
.catch(err => {
this.error.emit(err);
});
}
this.loadFolder();
});
}
public loadFolder() {
if (this.currentFolderId) {
this.loadFolderById(this.currentFolderId)
.catch(err => {
this.error.emit(err);
});
} else if (this.currentFolderPath) {
this.loadFolderByPath(this.currentFolderPath)
.catch(err => {
this.error.emit(err);
});
}
}
/**
* Gets a path for a given node.
* @param node