[ADF-459] Copy & move further features (#2187)

* Adding current folder to list by default

* Fix documentlist component's rowFilter and imageResolver

* Adding rowfilter to not show the current node in the list

* Removing unpermitted nodes from the selectable ones (not visually)

* Restore documentlist original behaviour (rowFilter and imageResolver)

* Select event interface works with array from this point on

* Introducing the one and only, mighty Breadcrumb

* Breadcrumb position fix

* Extract hightlight transform functionality from highlight pipe

* Highlight part I.

* Showing breadcrumb with the new redesigned functionality

* Rebase fix

* Error and success callback for the new content actions

* Tests for HighlightDirective

* Update documentation

* Until proper pagination we use this temporary fix

* Fix node unselection on folder change

* Fix accessibility support in dropdown breadcrumb
This commit is contained in:
Popovics András
2017-08-08 16:41:20 +01:00
committed by Eugenio Romano
parent 91f4a22ecc
commit 90f0aafb97
24 changed files with 793 additions and 184 deletions

View File

@@ -137,6 +137,8 @@
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.COPY' | translate}}"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
handler="copy">
</content-action>
<content-action
@@ -145,6 +147,8 @@
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.MOVE' | translate}}"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
handler="move">
</content-action>
<content-action
@@ -163,6 +167,8 @@
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.COPY' | translate}}"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
handler="copy">
</content-action>
<content-action
@@ -171,6 +177,8 @@
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.MOVE' | translate}}"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
handler="move">
</content-action>
<content-action

View File

@@ -20,7 +20,7 @@ import { MdDialog } from '@angular/material';
import { ActivatedRoute, Params } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api';
import {
AlfrescoApiService, AlfrescoContentService, FileUploadCompleteEvent,
AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, FileUploadCompleteEvent,
FolderCreatedEvent, NotificationService, SiteModel, UploadService
} from 'ng2-alfresco-core';
import { DocumentListComponent, PermissionStyleModel } from 'ng2-alfresco-documentlist';
@@ -89,6 +89,7 @@ export class FilesComponent implements OnInit {
private uploadService: UploadService,
private contentService: AlfrescoContentService,
private dialog: MdDialog,
private translateService: AlfrescoTranslationService,
@Optional() private route: ActivatedRoute) {
}
@@ -155,6 +156,29 @@ export class FilesComponent implements OnInit {
);
}
onContentActionError(errors) {
const errorStatusCode = JSON.parse(errors.message).error.statusCode;
let translatedErrorMessage: any;
switch (errorStatusCode) {
case 403:
translatedErrorMessage = this.translateService.get('OPERATION.ERROR.PERMISSION');
break;
case 409:
translatedErrorMessage = this.translateService.get('OPERATION.ERROR.CONFLICT');
break;
default:
translatedErrorMessage = this.translateService.get('OPERATION.ERROR.UNKNOWN');
}
this.notificationService.openSnackMessage(translatedErrorMessage.value, 4000);
}
onContentActionSuccess(message) {
let translatedMessage: any = this.translateService.get(message);
this.notificationService.openSnackMessage(translatedMessage.value, 4000);
}
onCreateFolderClicked(event: Event) {
let dialogRef = this.dialog.open(CreateFolderDialogComponent);
dialogRef.afterClosed().subscribe(folderName => {

View File

@@ -52,12 +52,15 @@ import { TranslationService } from './src/services/translation.service';
import { UploadService } from './src/services/upload.service';
import { UserPreferencesService } from './src/services/user-preferences.service';
import { HighlightDirective } from './src/directives/highlight.directive';
import { DeletedNodesApiService } from './src/services/deleted-nodes-api.service';
import { DiscoveryApiService } from './src/services/discovery-api.service';
import { FavoritesApiService } from './src/services/favorites-api.service';
import { HighlightTransformService } from './src/services/highlight-transform.service';
import { NodesApiService } from './src/services/nodes-api.service';
import { PeopleApiService } from './src/services/people-api.service';
import { SearchApiService } from './src/services/search-api.service';
import { SearchService } from './src/services/search.service';
import { SharedLinksApiService } from './src/services/shared-links-api.service';
import { SitesApiService } from './src/services/sites-api.service';
@@ -89,7 +92,7 @@ export { UpdateNotification } from './src/services/card-view-update.service';
export { ClickNotification } from './src/services/card-view-update.service';
export { AppConfigModule } from './src/services/app-config.service';
export { UserPreferencesService } from './src/services/user-preferences.service';
import { SearchService } from './src/services/search.service';
export { HighlightTransformService, HightlightTransformResult } from './src/services/highlight-transform.service';
export { DeletedNodesApiService } from './src/services/deleted-nodes-api.service';
export { FavoritesApiService } from './src/services/favorites-api.service';
@@ -121,6 +124,7 @@ export { CardViewItem } from './src/interface/card-view-item.interface';
export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component';
export * from './src/directives/upload.directive';
export * from './src/directives/highlight.directive';
export * from './src/utils/index';
export * from './src/events/base.event';
export * from './src/events/base-ui.event';
@@ -171,7 +175,8 @@ export function providers() {
SearchApiService,
SharedLinksApiService,
SitesApiService,
DiscoveryApiService
DiscoveryApiService,
HighlightTransformService
];
}
@@ -220,6 +225,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
...obsoleteMdlDirectives(),
UploadDirective,
NodePermissionDirective,
HighlightDirective,
DataColumnComponent,
DataColumnListComponent,
FileSizePipe,
@@ -243,6 +249,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
...obsoleteMdlDirectives(),
UploadDirective,
NodePermissionDirective,
HighlightDirective,
DataColumnComponent,
DataColumnListComponent,
FileSizePipe,

View File

@@ -0,0 +1,125 @@
/*!
* @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.
*/
import { Component, ViewChildren } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightTransformService } from '../services/highlight-transform.service';
import { HighlightDirective } from './highlight.directive';
const template: string = `
<div id="outerDiv1" adf-highlight adf-highlight-selector=".highlightable" adf-highlight-class="highlight-for-free-willy">
<div id="innerDiv11" class="highlightable">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
<div id="innerDiv12">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
<div id="innerDiv13" class="highlightable">consectetur adipiscing elit</div>
<div id="innerDiv14" class="highlightable">sed do eiusmod salana-eyong-aysis tempor incididunt</div>
</div>
<div id="outerDiv2" adf-highlight adf-highlight-selector=".highlightable">
<div id="innerDiv21" class="highlightable">Lorem ipsum salana-eyong-aysis dolor sit amet</div>
</div>`;
@Component({ selector: 'adf-test-component', template })
class TestComponent {
@ViewChildren(HighlightDirective) public hightlightDirectives;
}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
HighlightDirective,
TestComponent
],
providers: [
HighlightTransformService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should replace the searched text with the default hightlight class in the proper element (adf-highlight-selector)', () => {
component.hightlightDirectives.last.highlight('salana-eyong-aysis');
fixture.detectChanges();
const containerElement = fixture.debugElement.query(By.css('#innerDiv21'));
expect(containerElement).not.toBeNull();
expect(containerElement.nativeElement.innerHTML).toBe('Lorem ipsum <span class="adf-highlight">salana-eyong-aysis</span> dolor sit amet');
});
it('should replace the searched text with the default hightlight class in every proper element (highlight-for-free-willy)', () => {
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
fixture.detectChanges();
const containerElement1 = fixture.debugElement.query(By.css('#innerDiv11'));
const containerElement2 = fixture.debugElement.query(By.css('#innerDiv14'));
expect(containerElement1).not.toBeNull();
expect(containerElement2).not.toBeNull();
expect(containerElement1.nativeElement.innerHTML).toBe('Lorem ipsum <span class="highlight-for-free-willy">salana-eyong-aysis</span> dolor sit amet');
expect(containerElement2.nativeElement.innerHTML).toBe('sed do eiusmod <span class="highlight-for-free-willy">salana-eyong-aysis</span> tempor incididunt');
});
it('should NOT replace the searched text in an element without the proper selector class', () => {
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
fixture.detectChanges();
const containerElement1 = fixture.debugElement.query(By.css('#innerDiv12'));
expect(containerElement1).not.toBeNull();
expect(containerElement1.nativeElement.innerHTML).toBe('Lorem ipsum salana-eyong-aysis dolor sit amet');
});
it('should NOT reinsert the same text to the innerText if there was no change at all (search string is not found)', () => {
const highlighter = TestBed.get(HighlightTransformService);
spyOn(highlighter, 'highlight').and.returnValue({ changed: false, text: 'Modified text' });
component.hightlightDirectives.first.highlight('salana-eyong-aysis');
fixture.detectChanges();
const containerElement = fixture.debugElement.query(By.css('#innerDiv11'));
expect(containerElement).not.toBeNull();
expect(containerElement.nativeElement.innerHTML).not.toContain('Modified text');
});
it('should do the search only if there is a search string presented', () => {
const highlighter = TestBed.get(HighlightTransformService);
spyOn(highlighter, 'highlight').and.callThrough();
component.hightlightDirectives.first.highlight('');
fixture.detectChanges();
expect(highlighter.highlight).not.toHaveBeenCalled();
});
it('should do the search only if there is a node selector string presented', () => {
const highlighter = TestBed.get(HighlightTransformService);
spyOn(highlighter, 'highlight').and.callThrough();
const callback = function() {
component.hightlightDirectives.first.highlight('raddish', '');
fixture.detectChanges();
};
expect(callback).not.toThrowError();
expect(highlighter.highlight).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
/*!
* @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.
*/
import { Directive, ElementRef, Input, Renderer } from '@angular/core';
import { HighlightTransformService, HightlightTransformResult } from '../services/highlight-transform.service';
@Directive({
selector: '[adf-highlight]'
})
export class HighlightDirective {
@Input('adf-highlight-selector')
selector: string = '';
@Input('adf-highlight')
search: string = '';
@Input('adf-highlight-class')
classToApply: string = 'adf-highlight';
constructor(
private el: ElementRef,
private renderer: Renderer,
private highlightTransformService: HighlightTransformService) { }
public highlight(search = this.search, selector = this.selector, classToApply = this.classToApply) {
if (search && selector) {
const elements = this.el.nativeElement.querySelectorAll(selector);
elements.forEach((element) => {
const result: HightlightTransformResult = this.highlightTransformService.highlight(element.innerHTML, search, classToApply);
if (result.changed) {
this.renderer.setElementProperty(element, 'innerHTML', result.text);
}
});
}
}
}

View File

@@ -16,25 +16,17 @@
*/
import { Pipe, PipeTransform } from '@angular/core';
import { HighlightTransformService, HightlightTransformResult } from '../services/highlight-transform.service';
@Pipe({
name: 'highlight'
})
export class HighlightPipe implements PipeTransform {
constructor() { }
constructor(private highlightTransformService: HighlightTransformService) { }
transform(text: string, search: string): any {
if (search && text) {
let pattern = search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
pattern = pattern.split(' ').filter((t) => {
return t.length > 0;
}).join('|');
const regex = new RegExp(pattern, 'gi');
let result = text.replace(regex, (match) => `<span class="highlight">${match}</span>`);
return result;
} else {
return text;
}
transform(text: string, search: string): string {
const result: HightlightTransformResult = this.highlightTransformService.highlight(text, search);
return result.text;
}
}

View File

@@ -0,0 +1,44 @@
/*!
* @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.
*/
export interface HightlightTransformResult {
text: string;
changed: boolean;
}
export class HighlightTransformService {
public highlight(text: string, search: string, wrapperClass: string = 'highlight'): HightlightTransformResult {
let isMatching = false,
result = text;
if (search && text) {
let pattern = search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
pattern = pattern.split(' ').filter((t) => {
return t.length > 0;
}).join('|');
const regex = new RegExp(pattern, 'gi');
result = text.replace(regex, (match) => {
isMatching = true;
return `<span class="${wrapperClass}">${match}</span>`;
});
return { text: result, changed: isMatching};
} else {
return { text: result, changed: isMatching};
}
}
}

View File

@@ -33,6 +33,7 @@
- [Delete - Disable button checking the permission](#delete---disable-button-checking-the-permission)
- [Download](#download)
- [Copy and move](#copy-and-move)
* [Error, Permission and success callback](#error-permission-and-success-callback)
+ [Folder actions](#folder-actions)
* [Context Menu](#context-menu)
* [Navigation mode](#navigation-mode)
@@ -610,15 +611,17 @@ Properties:
| --- | --- | --- | --- |
| `target` | string | | "document" or "folder" |
| `title` | string | | The title of the action as shown in the menu |
| `handler` | string | | System type actions. Can be "delete" or "download" |
| `handler` | string | | System type actions. Can be "delete", "download", "copy" or "move" |
| `permission` | string | | The name of the permission |
Events:
| Name | Description |
| --- | --- |
| `execute` | Emitted when user clicks on the action. For combined handlers see below |
| `permissionEvent` | Emitted when a permission error happens |
| Name | Handler | Description |
| --- | --- | --- |
| `execute` | All | Emitted when user clicks on the action. For combined handlers see below |
| `permissionEvent` | All | Emitted when a permission error happens |
| `success` | copy, move | Emitted on successful action with the success string message |
| `error` | copy, move | Emitted on unsuccessful action with the error event |
DocumentList supports declarative actions for Documents and Folders.
Each action can be bound to either default out-of-the-box handler, to a custom behaviour or to both.
@@ -776,7 +779,7 @@ Initiates download of the corresponding document file.
##### Copy and move
Shows the destination chooser dialog for copy and move actions
Shows the destination chooser dialog for copy and move actions. By default the destination chooser lists all the folders of the subject item's parent (except the selected item which is about to be copied/moved if it was a folder itself also).
![Copy/move dialog](docs/assets/document-action-copymovedialog.png)
@@ -790,15 +793,21 @@ Shows the destination chooser dialog for copy and move actions
title="copy"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
(permissionEvent)="onPermissionsFailed($event)"
handler="copy">
</content-action>
<content-action
icon="redo"
target="document"
target="folder"
title="move"
permission="update"
[disableWithNoPermission]="true"
(error)="onContentActionError($event)"
(success)="onContentActionSuccess($event)"
(permissionEvent)="onPermissionsFailed($event)"
handler="move">
</content-action>
@@ -806,6 +815,14 @@ Shows the destination chooser dialog for copy and move actions
</adf-document-list>
```
###### Error, Permission and success callback
Defining error, permission and success callbacks are pretty much the same as doing it for the delete permission handling.
- The error handler callback gets the error object which was raised
- The success callback's only parameter is the translatable success message string (could be used for showing in snackbar for example)
- The permissionEvent callback is the same as described above with the delete action
![Copy/move document action](docs/assets/document-action-copymove.png)
#### Folder actions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -1,25 +1,28 @@
<ng-container *ngIf="route.length > 0">
<md-icon
<button
tabindex="0"
class="adf-dropdown-breadcumb-trigger"
[class.isRoot]="!hasPreviousNodes()"
(click)="open()"
data-automation-id="dropdown-breadcrumb-trigger">folder</md-icon>
data-automation-id="dropdown-breadcrumb-trigger">
<md-icon [class.isRoot]="!hasPreviousNodes()">folder</md-icon>
</button>
<i class="material-icons adf-dropddown-breadcrumb-item-chevron">chevron_right</i>
<md-select
*ngIf="hasPreviousNodes()"
class="adf-dropdown-breadcrumb-path"
tabindex="0"
data-automation-id="dropdown-breadcrumb-path" >
<md-option
*ngFor="let node of previousNodes;"
(click)="onRoutePathClick(node, $event)"
class="adf-dropdown-breadcrumb-path-option"
tabindex="0"
data-automation-class="dropdown-breadcrumb-path-option">
{{ node.name }}
</md-option>
</md-select>
<span

View File

@@ -11,6 +11,9 @@ $dropdownHorizontalOffset: 30px;
&-dropdown-breadcumb-trigger {
cursor: pointer;
padding: 0;
border: none;
background: transparent;
}
&-dropdown-breadcumb-trigger.isRoot {

View File

@@ -54,11 +54,11 @@ describe('ContentAction', () => {
beforeEach(() => {
contentService = TestBed.get(AlfrescoContentService);
translateService = <AlfrescoTranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, translateService, null, null);
nodeActionsService = new NodeActionsService(null, null, null);
notificationService = new NotificationService(null);
let documentServiceMock = new DocumentListServiceMock();
documentActions = new DocumentActionsService(translateService, notificationService, nodeActionsService);
folderActions = new FolderActionsService(translateService, notificationService, nodeActionsService, null, contentService);
documentActions = new DocumentActionsService(nodeActionsService);
folderActions = new FolderActionsService(nodeActionsService, null, contentService);
documentList = new DocumentListComponent(documentServiceMock, null, null, null);
actionList = new ContentActionListComponent(documentList);

View File

@@ -58,6 +58,12 @@ export class ContentActionComponent implements OnInit, OnChanges {
@Output()
permissionEvent = new EventEmitter();
@Output()
error = new EventEmitter();
@Output()
success = new EventEmitter();
model: ContentActionModel;
constructor(
@@ -111,6 +117,15 @@ export class ContentActionComponent implements OnInit, OnChanges {
this.documentActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
this.documentActions.error.subscribe((errors) => {
this.error.emit(errors);
});
this.documentActions.success.subscribe((message) => {
this.success.emit(message);
});
return this.documentActions.getHandler(name);
}
return null;
@@ -121,6 +136,15 @@ export class ContentActionComponent implements OnInit, OnChanges {
this.folderActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
this.folderActions.error.subscribe((errors) => {
this.error.emit(errors);
});
this.folderActions.success.subscribe((message) => {
this.success.emit(message);
});
return this.folderActions.getHandler(name);
}
return null;

View File

@@ -4,14 +4,13 @@
<section mdDialogContent
class="adf-content-node-selector-content"
(node-select)="onNodeSelect($event)"
(node-unselect)="onNodeUnselect()">
(node-select)="onNodeSelect($event)">
<md-input-container floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input #searchInput
mdInput
placeholder="Search"
(keyup)="search(searchInput.value)"
(input)="search(searchInput.value)"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
@@ -31,18 +30,34 @@
(change)="siteChanged($event)"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
<adf-toolbar>
<adf-toolbar-title>
<adf-dropdown-breadcrumb *ngIf="needBreadcrumbs()"
class="adf-content-node-selector-content-breadcrumb"
[target]="documentList"
[folderNode]="breadcrumbFolderNode"
data-automation-id="content-node-selector-content-breadcrumb">
</adf-dropdown-breadcrumb>
</adf-toolbar-title>
</adf-toolbar>
<div class="adf-content-node-selector-content-list" data-automation-id="content-node-selector-content-list">
<adf-document-list *ngIf="searched"
<adf-document-list
#documentList
adf-highlight
adf-highlight-selector=".cell-value adf-datatable-cell"
[node]="nodes"
[rowFilter]="rowFilter"
[imageResolver]="imageResolver"
[permissionsStyle]="permissionsStyle"
[creationMenuActions]="false"
[currentFolderId]="currentFolderId"
[currentFolderId]="folderIdToShow"
[selectionMode]="'single'"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
[enablePagination]="false"
[enablePagination]="!showingSearchResults"
(folderChange)="onFolderChange($event)"
data-automation-id="content-node-selector-document-list">
<empty-folder-content>
<ng-template>

View File

@@ -53,11 +53,19 @@
}
}
& /deep/ .adf-toolbar .mat-toolbar {
border: none;
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.07);
& /deep/ .adf-highlight {
color: #33afdf;
}
& /deep/ .adf-data-table {
border: none;

View File

@@ -16,14 +16,15 @@
*/
import { DebugElement, EventEmitter } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MD_DIALOG_DATA, MdDialogRef } from '@angular/material';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoTranslationService, CoreModule, SearchService, SiteModel } from 'ng2-alfresco-core';
import { AlfrescoContentService, AlfrescoTranslationService, CoreModule, SearchService, SiteModel } from 'ng2-alfresco-core';
import { DataTableModule } from 'ng2-alfresco-datatable';
import { MaterialModule } from '../../material.module';
import { DocumentListService } from '../../services/document-list.service';
import { DropdownBreadcrumbComponent } from '../breadcrumb/dropdown-breadcrumb.component';
import { DocumentListComponent } from '../document-list.component';
import { DocumentMenuActionComponent } from '../document-menu-action.component';
import { EmptyFolderContentDirective } from '../empty-folder/empty-folder-content.directive';
@@ -45,7 +46,6 @@ const ONE_FOLDER_RESULT = {
};
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorComponent;
let fixture: ComponentFixture<ContentNodeSelectorComponent>;
let element: DebugElement;
@@ -59,7 +59,7 @@ describe('ContentNodeSelectorComponent', () => {
function typeToSearchBox(searchTerm = 'string-to-search') {
let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]'));
searchInput.nativeElement.value = searchTerm;
searchInput.triggerEventHandler('keyup', {});
searchInput.triggerEventHandler('input', {});
fixture.detectChanges();
}
@@ -79,9 +79,11 @@ describe('ContentNodeSelectorComponent', () => {
DocumentMenuActionComponent,
EmptyFolderContentDirective,
DropdownSitesComponent,
DropdownBreadcrumbComponent,
ContentNodeSelectorComponent
],
providers: [
AlfrescoContentService,
AlfrescoTranslationService,
DocumentListService,
SearchService,
@@ -100,7 +102,10 @@ describe('ContentNodeSelectorComponent', () => {
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
select: new EventEmitter<MinimalNodeEntryEntity>()
select: new EventEmitter<MinimalNodeEntryEntity>(),
rowFilter: () => {},
imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku'
};
setupTestbed([{ provide: MD_DIALOG_DATA, useValue: data }]);
@@ -122,10 +127,29 @@ describe('ContentNodeSelectorComponent', () => {
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should pass through the injected currentFolderId to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the injected rowFilter to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter);
});
it('should pass through the injected imageResolver to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver);
});
it('should trigger the INJECTED select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
data.select.subscribe((node) => {
expect(node).toBe(expectedNode);
data.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
@@ -143,13 +167,13 @@ describe('ContentNodeSelectorComponent', () => {
});
it('should be shown if dialogRef is injected', () => {
const componentInstance = new ContentNodeSelectorComponent(null, null, data, dummyMdDialogRef);
const componentInstance = new ContentNodeSelectorComponent(null, null, null, data, dummyMdDialogRef);
expect(componentInstance.inDialog).toBeTruthy();
});
it('should should call the close method in the injected dialogRef', () => {
spyOn(dummyMdDialogRef, 'close');
const componentInstance = new ContentNodeSelectorComponent(null, null, data, dummyMdDialogRef);
const componentInstance = new ContentNodeSelectorComponent(null, null, null, data, dummyMdDialogRef);
componentInstance.close();
@@ -192,8 +216,9 @@ describe('ContentNodeSelectorComponent', () => {
it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
component.select.subscribe((node) => {
expect(node).toBe(expectedNode);
component.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
@@ -202,19 +227,131 @@ describe('ContentNodeSelectorComponent', () => {
});
});
describe('Breadcrumbs', () => {
let documentListService,
expectedDefaultFolderNode;
beforeEach(() => {
expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
documentListService = TestBed.get(DocumentListService);
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should show the breadcrumb for the currentFolderId by default', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
it('should not show the breadcrumb if search was performed as last action', (done) => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).toBeNull();
done();
});
});
it('should show the breadcrumb again on folder navigation in the results list', (done) => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
done();
});
});
it('should show the breadcrumb for the selected node when search results are displayed', (done) => {
const alfrescoContentService = TestBed.get(AlfrescoContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode} } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(chosenNode);
done();
});
});
it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => {
const alfrescoContentService = TestBed.get(AlfrescoContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode} } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
});
describe('Search functionality', () => {
function defaultSearchOptions(rootNodeId = undefined) {
return {
include: ['path', 'allowableOperations'],
skipCount: 0,
rootNodeId,
nodeType: 'cm:folder',
maxItems: 200,
orderBy: null
};
}
beforeEach(() => {
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should load the results by calling the search api on search change', () => {
typeToSearchBox('kakarot');
expect(searchSpy).toHaveBeenCalledWith('kakarot*', {
include: ['path'],
skipCount: 0,
rootNodeId: undefined,
nodeType: 'cm:folder',
maxItems: 40,
orderBy: null
});
expect(searchSpy).toHaveBeenCalledWith('kakarot*', defaultSearchOptions());
});
it('should reset the currently chosen node in case of starting a new search', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
typeToSearchBox('kakarot');
expect(component.chosenNode).toBeNull();
});
it('should NOT call the search api if the searchTerm length is less than 4 characters', () => {
@@ -236,14 +373,7 @@ describe('ContentNodeSelectorComponent', () => {
component.siteChanged(<SiteModel> { guid: 'namek' });
expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change');
expect(searchSpy.calls.argsFor(1)).toEqual(['vegeta*', {
include: ['path'],
skipCount: 0,
rootNodeId: 'namek',
nodeType: 'cm:folder',
maxItems: 40,
orderBy: null
}]);
expect(searchSpy.calls.argsFor(1)).toEqual(['vegeta*', defaultSearchOptions('namek') ]);
});
it('should show the search icon by default without the X (clear) icon', () => {
@@ -269,20 +399,43 @@ describe('ContentNodeSelectorComponent', () => {
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
component.nodes = [ component.chosenNode ];
component.searchTerm = 'whatever';
component.searched = true;
component.searchTerm = 'piccolo';
component.showingSearchResults = true;
component.clear();
expect(component.searched).toBe(false);
expect(component.searchTerm).toBe('');
expect(component.nodes).toEqual([]);
expect(component.chosenNode).toBeNull();
expect(component.showingSearchResults).toBeFalsy();
});
it('should show the default text instead of result list if search was not performed', () => {
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).toBeNull('Document list should not be shown by default');
it('should show the current folder\'s content instead of search results if search was not performed', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the rowFilter to the documentList', () => {
const filter = () => {};
component.rowFilter = filter;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(filter);
});
it('should pass through the imageResolver to the documentList', () => {
const resolver = () => 'piccolo';
component.imageResolver = resolver;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(resolver);
});
it('should show the result list when search was performed', async(() => {
@@ -292,10 +445,21 @@ describe('ContentNodeSelectorComponent', () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown after search');
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBeNull();
});
}));
it('should highlight the results when search was performed in the next timeframe', fakeAsync(() => {
spyOn(component.highlighter, 'highlight');
typeToSearchBox('shenron');
respondWithSearchResults(ONE_FOLDER_RESULT);
expect(component.highlighter.highlight).not.toHaveBeenCalled();
tick();
expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron');
}));
it('should show the default text instead of result list if search was cleared', async(() => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
@@ -308,7 +472,8 @@ describe('ContentNodeSelectorComponent', () => {
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).toBeNull('Document list should NOT be shown after clearing the search');
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
}));
@@ -331,6 +496,14 @@ describe('ContentNodeSelectorComponent', () => {
describe('Choose button', () => {
const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {};
let hasPermission;
beforeEach(() => {
const alfrescoContentService = TestBed.get(AlfrescoContentService);
spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission);
});
it('should be disabled by default', () => {
fixture.detectChanges();
@@ -338,21 +511,57 @@ describe('ContentNodeSelectorComponent', () => {
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should be enabled when clicking on one element in the list (onNodeSelect)', () => {
fixture.detectChanges();
it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(false);
});
it('should be disabled when deselecting the previously selected element in the list (onNodeUnselect)', () => {
it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => {
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should become disabled when changing directory after previously selecting a right node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should be disabled when resetting the chosen node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
fixture.detectChanges();
component.onNodeUnselect();
component.resetChosenNode();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));

View File

@@ -15,14 +15,19 @@
* limitations under the License.
*/
import { Component, EventEmitter, Inject, Input, Optional, Output, ViewEncapsulation } from '@angular/core';
import { Component, EventEmitter, Inject, Input, OnInit, Optional, Output, ViewChild, ViewEncapsulation } from '@angular/core';
import { MD_DIALOG_DATA, MdDialogRef } from '@angular/material';
import { MinimalNodeEntryEntity, NodePaging } from 'alfresco-js-api';
import { AlfrescoTranslationService, SearchOptions, SearchService, SiteModel } from 'ng2-alfresco-core';
import { AlfrescoContentService, AlfrescoTranslationService, HighlightDirective, SearchOptions, SearchService, SiteModel } from 'ng2-alfresco-core';
import { ImageResolver, RowFilter } from '../../data/share-datatable-adapter';
import { DocumentListComponent } from '../document-list.component';
export interface ContentNodeSelectorComponentData {
title: string;
select: EventEmitter<MinimalNodeEntryEntity>;
currentFolderId?: string;
rowFilter?: RowFilter;
imageResolver?: ImageResolver;
select: EventEmitter<MinimalNodeEntryEntity[]>;
}
@Component({
@@ -31,24 +36,41 @@ export interface ContentNodeSelectorComponentData {
templateUrl: './content-node-selector.component.html',
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorComponent {
export class ContentNodeSelectorComponent implements OnInit {
nodes: NodePaging|Array<any>;
siteId: null|string;
nodes: NodePaging | Array<any>;
siteId: null | string;
searchTerm: string = '';
searched: boolean = false;
showingSearchResults: boolean = false;
inDialog: boolean = false;
chosenNode: MinimalNodeEntryEntity | null = null;
folderIdToShow: string | null = null;
@Input()
title: string;
@Input()
currentFolderId: string | null = null;
@Input()
rowFilter: RowFilter = null;
@Input()
imageResolver: ImageResolver = null;
@Output()
select: EventEmitter<MinimalNodeEntryEntity> = new EventEmitter<MinimalNodeEntryEntity>();
select: EventEmitter<MinimalNodeEntryEntity[]> = new EventEmitter<MinimalNodeEntryEntity[]>();
@ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(HighlightDirective)
highlighter: HighlightDirective;
constructor(private searchService: SearchService,
private contentService: AlfrescoContentService,
@Optional() translateService: AlfrescoTranslationService,
@Optional() @Inject(MD_DIALOG_DATA) public data?: ContentNodeSelectorComponentData,
@Optional() @Inject(MD_DIALOG_DATA) data?: ContentNodeSelectorComponentData,
@Optional() private containingDialog?: MdDialogRef<ContentNodeSelectorComponent>) {
if (translateService) {
@@ -58,13 +80,20 @@ export class ContentNodeSelectorComponent {
if (data) {
this.title = data.title;
this.select = data.select;
this.currentFolderId = data.currentFolderId;
this.rowFilter = data.rowFilter;
this.imageResolver = data.imageResolver;
}
if (containingDialog) {
if (this.containingDialog) {
this.inDialog = true;
}
}
ngOnInit() {
this.folderIdToShow = this.currentFolderId;
}
/**
* Updates the site attribute and starts a new search
*
@@ -85,14 +114,33 @@ export class ContentNodeSelectorComponent {
this.querySearch();
}
needBreadcrumbs() {
const whenInFolderNavigation = !this.showingSearchResults,
whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode;
return whenInFolderNavigation || whenInSelectingSearchResult;
}
/**
* Returns the actually selected|entered folder node or null in case of searching for the breadcrumb
*/
get breadcrumbFolderNode(): MinimalNodeEntryEntity|null {
if (this.showingSearchResults && this.chosenNode) {
return this.chosenNode;
} else {
return this.documentList.folderNode;
}
}
/**
* Clear the search input
*/
clear(): void {
this.searched = false;
this.searchTerm = '';
this.nodes = [];
this.chosenNode = null;
this.showingSearchResults = false;
this.folderIdToShow = this.currentFolderId;
}
/**
@@ -100,39 +148,65 @@ export class ContentNodeSelectorComponent {
*/
private querySearch(): void {
if (this.searchTerm.length > 3) {
this.chosenNode = null;
const searchTerm = this.searchTerm + '*';
let searchOpts: SearchOptions = {
include: ['path'],
include: ['path', 'allowableOperations'],
skipCount: 0,
rootNodeId: this.siteId,
nodeType: 'cm:folder',
maxItems: 40,
maxItems: 200,
orderBy: null
};
this.searchService
.getNodeQueryResults(searchTerm, searchOpts)
.subscribe(
results => {
this.searched = true;
this.showingSearchResults = true;
this.folderIdToShow = null;
this.nodes = results;
this.highlight();
}
);
}
}
/**
* Hightlight the actual searchterm in the next frame
*/
highlight() {
setTimeout(() => {
this.highlighter.highlight(this.searchTerm);
}, 0);
}
/**
* Invoked when user selects a node
*
* @param event CustomEvent for node-select
*/
onNodeSelect(event: any): void {
this.chosenNode = event.detail.node.entry;
const entry: MinimalNodeEntryEntity = event.detail.node.entry;
if (this.contentService.hasPermission(entry, 'update')) {
this.chosenNode = entry;
} else {
this.resetChosenNode();
}
}
/**
* * Invoked when user unselects a node
* Sets showingSearchResults state to be able to differentiate between search results or folder results
*/
onNodeUnselect(): void {
onFolderChange() {
this.showingSearchResults = false;
this.chosenNode = null;
}
/**
* Clears the chosen node
*/
resetChosenNode(): void {
this.chosenNode = null;
}
@@ -140,7 +214,8 @@ export class ContentNodeSelectorComponent {
* Emit event with the chosen node
*/
choose(): void {
this.select.next(this.chosenNode);
// Multiple selections to be implemented...
this.select.next([this.chosenNode]);
}
/**

View File

@@ -728,20 +728,33 @@ describe('DocumentList', () => {
expect(element.querySelector('alfresco-pagination')).toBe(null);
});
it('should set row filter for underlying adapter', () => {
it('should set row filter and reload contents if currentFolderId is set when setting rowFilter', () => {
let filter = <RowFilter> {};
documentList.currentFolderId = 'id';
spyOn(documentList.data, 'setFilter').and.callThrough();
spyOn(documentListService, 'getFolder');
documentList.ngOnChanges({rowFilter: new SimpleChange(null, filter, true)});
documentList.rowFilter = filter;
expect(documentList.data.setFilter).toHaveBeenCalledWith(filter);
expect(documentListService.getFolder).toHaveBeenCalled();
});
it('should NOT reload contents if currentFolderId is NOT set when setting rowFilter', () => {
documentList.currentFolderId = null;
spyOn(documentListService, 'getFolder');
documentList.ngOnChanges({rowFilter: new SimpleChange(null, <RowFilter> {}, true)});
expect(documentListService.getFolder).not.toHaveBeenCalled();
});
it('should set image resolver for underlying adapter', () => {
let resolver = <ImageResolver> {};
spyOn(documentList.data, 'setImageResolver').and.callThrough();
documentList.imageResolver = resolver;
documentList.ngOnChanges({imageResolver: new SimpleChange(null, resolver, true)});
expect(documentList.data.setImageResolver).toHaveBeenCalledWith(resolver);
});

View File

@@ -103,19 +103,10 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
pagination: Pagination;
@Input()
set rowFilter(value: RowFilter) {
if (this.data && value && this.currentFolderId) {
this.data.setFilter(value);
this.loadFolderNodesByFolderNodeId(this.currentFolderId, this.pageSize, this.skipCount);
}
}
rowFilter: RowFilter|null = null;
@Input()
set imageResolver(value: ImageResolver) {
if (this.data) {
this.data.setImageResolver(value);
}
}
imageResolver: ImageResolver|null = null;
// The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root-
@Input()
@@ -198,8 +189,16 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
ngOnInit() {
this.data = new ShareDataTableAdapter(this.documentListService, null, this.getDefaultSorting());
this.data.thumbnails = this.thumbnails;
this.data.permissionsStyle = this.permissionsStyle;
if (this.rowFilter) {
this.data.setFilter(this.rowFilter);
}
if (this.imageResolver) {
this.data.setImageResolver(this.imageResolver);
}
this.contextActionHandler.subscribe(val => this.contextActionCallback(val));
this.enforceSingleClickNavigationForMobile();
@@ -229,9 +228,16 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
this.loadFolder();
} else if (changes['currentFolderId'] && changes['currentFolderId'].currentValue) {
this.loadFolderByNodeId(changes['currentFolderId'].currentValue);
} else if (changes['node'] && changes['node'].currentValue) {
if (this.data) {
} else if (this.data) {
if (changes['node'] && changes['node'].currentValue) {
this.data.loadPage(changes['node'].currentValue);
} else if (changes['rowFilter']) {
this.data.setFilter(changes['rowFilter'].currentValue);
if (this.currentFolderId) {
this.loadFolderNodesByFolderNodeId(this.currentFolderId, this.pageSize, this.skipCount);
}
} else if (changes['imageResolver']) {
this.data.setImageResolver(changes['imageResolver'].currentValue);
}
}
}

View File

@@ -34,7 +34,8 @@
},
"ERROR": {
"CONFLICT": "Name already exists in target location.",
"UNKNOWN": "Unknown error happened."
"UNKNOWN": "Unknown error happened.",
"PERMISSION": "You don't have the permission to perform this action."
}
}
}

View File

@@ -36,9 +36,9 @@ describe('DocumentActionsService', () => {
documentListService = new DocumentListServiceMock();
contentService = new AlfrescoContentService(null, null, null);
translateService = <AlfrescoTranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, translateService, null, null);
nodeActionsService = new NodeActionsService(null, null, null);
notificationService = new NotificationService(null);
service = new DocumentActionsService(translateService, notificationService, nodeActionsService, documentListService, contentService);
service = new DocumentActionsService(nodeActionsService, documentListService, contentService);
});
it('should register default download action', () => {
@@ -70,7 +70,7 @@ describe('DocumentActionsService', () => {
let file = new FileNode();
expect(service.canExecuteAction(file)).toBeTruthy();
service = new DocumentActionsService(translateService, notificationService, nodeActionsService);
service = new DocumentActionsService(nodeActionsService);
expect(service.canExecuteAction(file)).toBeFalsy();
});
@@ -191,7 +191,7 @@ describe('DocumentActionsService', () => {
});
it('should require internal service for download action', () => {
let actionService = new DocumentActionsService(translateService, notificationService, nodeActionsService, null, contentService);
let actionService = new DocumentActionsService(nodeActionsService, null, contentService);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
result.subscribe((value) => {
@@ -200,7 +200,7 @@ describe('DocumentActionsService', () => {
});
it('should require content service for download action', () => {
let actionService = new DocumentActionsService(translateService, notificationService, nodeActionsService, documentListService, null);
let actionService = new DocumentActionsService(nodeActionsService, documentListService, null);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
result.subscribe((value) => {

View File

@@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { AlfrescoContentService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Rx';
import { ContentActionHandler } from '../models/content-action.model';
@@ -29,18 +29,15 @@ import { NodeActionsService } from './node-actions.service';
export class DocumentActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
error: Subject<Error> = new Subject<Error>();
success: Subject<string> = new Subject<string>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private translateService: AlfrescoTranslationService,
private notificationService: NotificationService,
private nodeActionsService: NodeActionsService,
constructor(private nodeActionsService: NodeActionsService,
private documentListService?: DocumentListService,
private contentService?: AlfrescoContentService) {
this.setupActionHandlers();
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist');
}
}
getHandler(key: string): ContentActionHandler {
@@ -99,25 +96,12 @@ export class DocumentActionsService {
private prepareHandlers(actionObservable, type: string, action: string, target?: any, permission?: string): void {
actionObservable.subscribe(
(fileOperationMessage) => {
this.notificationService.openSnackMessage(fileOperationMessage, 3000);
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(fileOperationMessage);
},
(errorStatusCode) => {
switch (errorStatusCode) {
case 403:
this.permissionEvent.next(new PermissionModel({type, action, permission}));
break;
case 409:
let conflictError: any = this.translateService.get('OPERATION.ERROR.CONFLICT');
this.notificationService.openSnackMessage(conflictError.value, 3000);
break;
default:
let unknownError: any = this.translateService.get('OPERATION.ERROR.UNKNOWN');
this.notificationService.openSnackMessage(unknownError.value, 3000);
}
}
this.error.next.bind(this.error)
);
}

View File

@@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { AlfrescoContentService } from 'ng2-alfresco-core';
import { Observable, Subject } from 'rxjs/Rx';
import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
@@ -28,12 +28,12 @@ import { NodeActionsService } from './node-actions.service';
export class FolderActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
error: Subject<Error> = new Subject<Error>();
success: Subject<string> = new Subject<string>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private translateService: AlfrescoTranslationService,
private notificationService: NotificationService,
private nodeActionsService: NodeActionsService,
constructor(private nodeActionsService: NodeActionsService,
private documentListService: DocumentListService,
private contentService: AlfrescoContentService) {
this.setupActionHandlers();
@@ -81,25 +81,12 @@ export class FolderActionsService {
private prepareHandlers(actionObservable, type: string, action: string, target?: any, permission?: string): void {
actionObservable.subscribe(
(fileOperationMessage) => {
this.notificationService.openSnackMessage(fileOperationMessage, 3000);
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(fileOperationMessage);
},
(errorStatusCode) => {
switch (errorStatusCode) {
case 403:
this.permissionEvent.next(new PermissionModel({type, action, permission}));
break;
case 409:
let conflictError: any = this.translateService.get('OPERATION.ERROR.CONFLICT');
this.notificationService.openSnackMessage(conflictError.value, 3000);
break;
default:
let unknownError: any = this.translateService.get('OPERATION.ERROR.UNKNOWN');
this.notificationService.openSnackMessage(unknownError.value, 3000);
}
}
this.error.next.bind(this.error)
);
}

View File

@@ -18,22 +18,19 @@
import { EventEmitter, Injectable } from '@angular/core';
import { MdDialog } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService } from 'ng2-alfresco-core';
import { AlfrescoContentService } from 'ng2-alfresco-core';
import { DataColumn } from 'ng2-alfresco-datatable';
import { Subject } from 'rxjs/Rx';
import { ContentNodeSelectorComponent } from '../components/content-node-selector/content-node-selector.component';
import { ContentNodeSelectorComponent, ContentNodeSelectorComponentData } from '../components/content-node-selector/content-node-selector.component';
import { ShareDataRow } from '../data/share-datatable-adapter';
import { DocumentListService } from './document-list.service';
@Injectable()
export class NodeActionsService {
constructor(private dialog: MdDialog,
private translateService: AlfrescoTranslationService,
private documentListService?: DocumentListService,
private contentService?: AlfrescoContentService) {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist');
}
}
private contentService?: AlfrescoContentService) {}
/**
* Copy content node
@@ -87,34 +84,49 @@ export class NodeActionsService {
const observable: Subject<string> = new Subject<string>();
if (this.contentService.hasPermission(contentEntry, permission)) {
const title = `${action} ${contentEntry.name} to ...`,
select: EventEmitter<MinimalNodeEntryEntity> = new EventEmitter<MinimalNodeEntryEntity>();
const data: ContentNodeSelectorComponentData = {
title: `${action} ${contentEntry.name} to ...`,
currentFolderId: contentEntry.parentId,
rowFilter: this.rowFilter.bind(this, contentEntry.id),
imageResolver: this.imageResolver.bind(this),
select: new EventEmitter<MinimalNodeEntryEntity[]>()
};
this.dialog.open(ContentNodeSelectorComponent, {
data: { title, select },
panelClass: 'adf-content-node-selector-dialog',
width: '576px'
});
this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: 'adf-content-node-selector-dialog', width: '576px' });
select.subscribe((parent: MinimalNodeEntryEntity) => {
this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, parent.id)
data.select.subscribe((selections: MinimalNodeEntryEntity[]) => {
const selection = selections[0];
this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, selection.id)
.subscribe(
() => {
let fileOperationMessage: any = this.translateService.get(`OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`);
observable.next(fileOperationMessage.value);
},
(errors) => {
const errorStatusCode = JSON.parse(errors.message).error.statusCode;
observable.error(errorStatusCode);
}
observable.next.bind(observable, `OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`),
observable.error.bind(observable)
);
this.dialog.closeAll();
});
return observable;
} else {
observable.error(403);
observable.error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
return observable;
}
}
private rowFilter(currentNodeId, row: ShareDataRow): boolean {
const node: MinimalNodeEntryEntity = row.node.entry;
if (node.id === currentNodeId || node.isFile) {
return false;
} else {
return true;
}
}
private imageResolver(row: ShareDataRow, col: DataColumn): string|null {
const entry: MinimalNodeEntryEntity = row.node.entry;
if (!this.contentService.hasPermission(entry, 'update')) {
return this.documentListService.getMimeTypeIcon('disable/folder');
}
return null;
}
}