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

@@ -54,6 +54,11 @@ export const appRoutes: Routes = [
component: FilesComponent,
canActivate: [AuthGuardEcm]
},
{
path: 'files/:id',
component: FilesComponent,
canActivate: [AuthGuardEcm]
},
{
path: 'datatable',
component: DataTableDemoComponent,

View File

@@ -5,7 +5,7 @@
(onSuccess)="documentList.reload()">
<alfresco-document-list-breadcrumb
[currentFolderPath]="currentPath"
[target]="documentList">
(pathChanged)="onBreadcrumbPathChanged($event)" *ngIf="!currentFolderId">
</alfresco-document-list-breadcrumb>
<div *ngIf="errorMessage" class="error-message">
<button (click)="resetError()" class="mdl-button mdl-js-button mdl-button--icon">
@@ -15,7 +15,9 @@
</div>
<alfresco-document-list
#documentList
[rootFolderId]="rootFolderId"
[currentFolderPath]="currentPath"
[currentFolderId]="currentFolderId"
[contextMenuActions]="true"
[contentActions]="true"
(error)="onNavigationError($event)"

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit, Optional, ViewChild } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AlfrescoAuthenticationService } from 'ng2-alfresco-core';
import {
DocumentActionsService,
@@ -34,6 +34,8 @@ import { FormService } from 'ng2-activiti-form';
})
export class FilesComponent implements OnInit {
currentPath: string = '/Sites/swsdp/documentLibrary';
rootFolderId: string = '-root-';
currentFolderId: string = null;
errorMessage: string = null;
fileNodeId: any;
@@ -51,7 +53,8 @@ export class FilesComponent implements OnInit {
constructor(private documentActions: DocumentActionsService,
public auth: AlfrescoAuthenticationService,
private formService: FormService,
private router: Router) {
private router: Router,
@Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this));
}
@@ -82,6 +85,12 @@ export class FilesComponent implements OnInit {
}
}
onBreadcrumbPathChanged(event?: any) {
if (event) {
this.currentPath = event.value;
}
}
toggleMultipleFileUpload() {
this.multipleFileUpload = !this.multipleFileUpload;
return this.multipleFileUpload;
@@ -104,7 +113,12 @@ export class FilesComponent implements OnInit {
}
ngOnInit() {
if ( this.auth.isBpmLoggedIn() ) {
if (this.route) {
this.route.params.forEach((params: Params) => {
this.currentFolderId = params.hasOwnProperty('id') ? params['id'] : null;
});
}
if (this.auth.isBpmLoggedIn()) {
this.formService.getProcessDefinitions().subscribe(
defs => this.setupBpmActions(defs || []),
err => console.log(err)

View File

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

View File

@@ -18,6 +18,7 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Router } from '@angular/router';
import { AlfrescoAuthenticationService } from 'ng2-alfresco-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
@Component({
selector: 'search-bar',
@@ -51,10 +52,12 @@ export class SearchBarComponent {
}]);
}
onFileClicked(event) {
if (event.value.entry.isFile) {
this.fileNodeId = event.value.entry.id;
onItemClicked(event: MinimalNodeEntity) {
if (event.entry.isFile) {
this.fileNodeId = event.entry.id;
this.fileShowed = true;
} else if (event.entry.isFolder) {
this.router.navigate(['/files', event.entry.id]);
}
}

View File

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

View File

@@ -16,6 +16,8 @@
*/
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api';
@Component({
selector: 'search-component',
@@ -48,10 +50,15 @@ export class SearchComponent {
fileShowed: boolean = false;
fileNodeId: string;
onFileClicked(event) {
if (event.value.entry.isFile) {
this.fileNodeId = event.value.entry.id;
constructor(public router: Router) {
}
onNavigateItem(event: MinimalNodeEntity) {
if (event.entry.isFile) {
this.fileNodeId = event.entry.id;
this.fileShowed = true;
} else if (event.entry.isFolder) {
this.router.navigate(['/files', event.entry.id]);
}
}
}

View File

@@ -168,7 +168,9 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `rootPath` | string | -root- | Root node path, i.e. `-root-`, `-shared-`, `-my-`, etc. |
| `rootFolderId` | string | -root- | Root node ID, i.e. `-root-`, `-shared-`, `-my-`, etc. or a fixed node ID |
| `currentFolderPath` | string | null | Initial path of displayed folder below the root node, e.g. "/Sites/swsdp/documentLibrary" |
| `currentFolderId` | string | null | Initial node ID of displayed folder, if given |
| `navigate` | boolean | true | Toggles navigation to folder content or file preview |
| `navigationMode` | string (click\|dblclick) | dblclick | User interaction for folder navigation or file preview |
| `thumbnails` | boolean | false | Show document thumbnails rather than icons |

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,19 +327,32 @@ 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)
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);
});
}
});
}
/**

View File

@@ -31,7 +31,7 @@ export class ShareDataTableAdapter implements DataTableAdapter, PaginationProvid
ERR_ROW_NOT_FOUND: string = 'Row not found';
ERR_COL_NOT_FOUND: string = 'Column not found';
DEFAULT_ROOT_PATH: string = '-root-';
DEFAULT_ROOT_ID: string = '-root-';
DEFAULT_DATE_FORMAT: string = 'medium';
DEFAULT_PAGE_SIZE: number = 20;
MIN_PAGE_SIZE: number = 5;
@@ -53,7 +53,7 @@ export class ShareDataTableAdapter implements DataTableAdapter, PaginationProvid
thumbnails: boolean = false;
dataLoaded: DataLoadedEventEmitter;
rootPath: string = this.DEFAULT_ROOT_PATH;
rootFolderId: string = this.DEFAULT_ROOT_ID;
constructor(private documentListService: DocumentListService,
private basePath: string,
@@ -210,7 +210,7 @@ export class ShareDataTableAdapter implements DataTableAdapter, PaginationProvid
.getFolder(path, {
maxItems: this._maxItems,
skipCount: this._skipCount,
rootPath: this.rootPath
rootFolderId: this.rootFolderId
})
.subscribe(val => {
this.currentPath = path;
@@ -228,6 +228,30 @@ export class ShareDataTableAdapter implements DataTableAdapter, PaginationProvid
}
loadById(id: string): Promise<any> {
return new Promise((resolve, reject) => {
if (id && this.documentListService) {
this.documentListService
.getFolder(null, {
maxItems: this._maxItems,
skipCount: this._skipCount,
rootFolderId: id
})
.subscribe(val => {
this.loadPage(<NodePaging>val);
this.dataLoaded.emit(null);
resolve(true);
},
error => {
reject(error);
});
} else {
resolve(false);
}
});
}
setFilter(filter: RowFilter) {
this.filter = filter;

View File

@@ -30,6 +30,8 @@ export class DocumentListService {
static DEFAULT_MIME_TYPE_ICON: string = 'ft_ic_miscellaneous.svg';
static ROOT_ID = '-root-';
mimeTypeIcons: any = {
'image/png': 'ft_ic_raster_image.svg',
'image/jpeg': 'ft_ic_raster_image.svg',
@@ -63,17 +65,21 @@ export class DocumentListService {
}
private getNodesPromise(folder: string, opts?: any): Promise<NodePaging> {
let rootPath = '-root-';
if (opts && opts.rootPath) {
rootPath = opts.rootPath;
let rootNodeId = DocumentListService.ROOT_ID;
if (opts && opts.rootFolderId) {
rootNodeId = opts.rootFolderId;
}
let params: any = {
relativePath: folder,
includeSource: true,
include: ['path', 'properties']
};
if (folder) {
params.relativePath = folder;
}
if (opts) {
if (opts.maxItems) {
params.maxItems = opts.maxItems;
@@ -83,7 +89,7 @@ export class DocumentListService {
}
}
return this.apiService.getInstance().nodes.getNodeChildren(rootPath, params);
return this.apiService.getInstance().nodes.getNodeChildren(rootNodeId, params);
}
deleteNode(nodeId: string): Observable<any> {
@@ -105,7 +111,7 @@ export class DocumentListService {
}
/**
* Gets the folder node with the content.
* Gets the folder node with the specified relative name path below the root node.
* @param folder Path to folder.
* @param opts Options.
* @returns {Observable<NodePaging>} Folder entity.

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');
});
describe('click results', () => {
beforeEach(() => {
component.navigationMode = AlfrescoSearchComponent.SINGLE_CLICK_NAVIGATION;
});
it('should emit navigation event when file item clicked', (done) => {
component.resultsLoad.subscribe(() => {
fixture.detectChanges();
(<HTMLTableRowElement> element.querySelector('#result_row_0')).click();
});
component.searchTerm = 'searchTerm';
component.ngOnInit();
component.preview.subscribe(() => {
done();
});
});
it('should not emit preview when non-file item is clicked', (done) => {
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();
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();
});
});
describe('double click results', () => {
beforeEach(() => {
component.navigationMode = AlfrescoSearchComponent.DOUBLE_CLICK_NAVIGATION;
});
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) {
if (node.entry.isFile) {
this.preview.emit({
value: node
});
this.navigate.emit(node);
}
}
}
onItemDblClick(node: MinimalNodeEntity) {
if (this.navigate && this.navigationMode === AlfrescoSearchComponent.DOUBLE_CLICK_NAVIGATION) {
if (node && node.entry) {
this.navigate.emit(node);
}
}
}