[ADF-1041] Node picker, first iteration (#2122)

* First try

* Dialog basic functionality

* Search input

* Hammering it together

* Fist working proto for copy

* Fix the tests and tslint errors for a happier world

* Add more tests (and test shells for the future)

* copyNode and moveNode methods

* Copy and move actions for content type

* Extract common parts in favor of using them in folder content type also

* Small fixes

* Copy and Move actions for folders as well

* Style fixes, ui behaviours and tests needed to be written

* Move duplicated search service from documentlist to core

* Use search service from core within the search component

* Fix dialog width

* Update docs

* Tests for node selector

* Change seletionMade event's name to select
This commit is contained in:
Popovics András
2017-07-25 09:17:11 +01:00
committed by Eugenio Romano
parent 952da3ab99
commit 4fd8bfb875
33 changed files with 1218 additions and 138 deletions

View File

@@ -117,6 +117,22 @@
<content-actions> <content-actions>
<!-- folder actions --> <!-- folder actions -->
<content-action
icon="content_copy"
target="folder"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.COPY' | translate}}"
permission="update"
[disableWithNoPermission]="true"
handler="copy">
</content-action>
<content-action
icon="redo"
target="folder"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.MOVE' | translate}}"
permission="update"
[disableWithNoPermission]="true"
handler="move">
</content-action>
<content-action <content-action
icon="delete" icon="delete"
target="folder" target="folder"
@@ -133,6 +149,22 @@
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}" title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}"
handler="download"> handler="download">
</content-action> </content-action>
<content-action
icon="content_copy"
target="document"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.COPY' | translate}}"
permission="update"
[disableWithNoPermission]="true"
handler="copy">
</content-action>
<content-action
icon="redo"
target="document"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.MOVE' | translate}}"
permission="update"
[disableWithNoPermission]="true"
handler="move">
</content-action>
<content-action <content-action
icon="delete" icon="delete"
target="document" target="document"

View File

@@ -28,7 +28,7 @@ const alfrescoLibs = [
module.exports = webpackMerge(commonConfig, { module.exports = webpackMerge(commonConfig, {
devtool: 'cheap-module-eval-source-map', devtool: 'cheap-module-source-map',
output: { output: {
path: helpers.root('dist'), path: helpers.root('dist'),

View File

@@ -10,15 +10,15 @@
}, },
"ACTIONS": { "ACTIONS": {
"FOLDER": { "FOLDER": {
"SYSTEM_1": "System folder action 1", "COPY": "Copy",
"CUSTOM": "Custom folder action", "MOVE": "Move",
"DELETE": "Delete" "DELETE": "Delete"
}, },
"DOCUMENT": { "DOCUMENT": {
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"DELETE": "Delete", "COPY": "Copy",
"SYSTEM_2": "System document action 2", "MOVE": "Move",
"CUSTOM": "Custom action" "DELETE": "Delete"
} }
} }
}, },

View File

@@ -85,6 +85,7 @@ export { UpdateNotification } from './src/services/card-view-update.service';
export { ClickNotification } 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 { AppConfigModule } from './src/services/app-config.service';
export { UserPreferencesService } from './src/services/user-preferences.service'; export { UserPreferencesService } from './src/services/user-preferences.service';
import { SearchService } from './src/services/search.service';
export { DeletedNodesApiService } from './src/services/deleted-nodes-api.service'; export { DeletedNodesApiService } from './src/services/deleted-nodes-api.service';
export { FavoritesApiService } from './src/services/favorites-api.service'; export { FavoritesApiService } from './src/services/favorites-api.service';

View File

@@ -0,0 +1,63 @@
/*!
* @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 let fakeSearch = {
list: {
pagination: {
count: 1,
hasMoreItems: false,
totalItems: 1,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
id: '123',
name: 'MyDoc',
content: {
mimetype: 'text/plain'
},
createdByUser: {
displayName: 'John Doe'
},
modifiedByUser: {
displayName: 'John Doe'
}
}
}
]
}
};
export let fakeError = {
error: {
errorKey: 'Search failed',
statusCode: 400,
briefSummary: '08220082 search failed',
stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
descriptionURL: 'https://api-explorer.alfresco.com'
}
};
export let fakeApi = {
core: {
queriesApi: {
findNodes: (term, opts) => Promise.resolve(fakeSearch)
}
}
};

View File

@@ -16,9 +16,17 @@
*/ */
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { AlfrescoApiService, CoreModule } from 'ng2-alfresco-core';
import { fakeApi, fakeError, fakeSearch } from '../assets/search.service.mock'; import { fakeApi, fakeError, fakeSearch } from '../assets/search.service.mock';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { AlfrescoApiService } from './alfresco-api.service';
import { AlfrescoSettingsService } from './alfresco-settings.service';
import { AppConfigModule } from './app-config.service';
import { AuthenticationService } from './authentication.service';
import { CookieService } from './cookie.service';
import { LogService } from './log.service';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { StorageService } from './storage.service';
import { UserPreferencesService } from './user-preferences.service';
declare let jasmine: any; declare let jasmine: any;
@@ -30,10 +38,18 @@ describe('SearchService', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CoreModule AppConfigModule
], ],
providers: [ providers: [
SearchService SearchService,
AuthenticationService,
AlfrescoApiService,
AlfrescoSettingsService,
AuthenticationService,
StorageService,
UserPreferencesService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
] ]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -17,8 +17,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { NodePaging } from 'alfresco-js-api'; import { NodePaging } from 'alfresco-js-api';
import { AlfrescoApiService, AlfrescoAuthenticationService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { AlfrescoApiService } from './alfresco-api.service';
import { AuthenticationService } from './authentication.service';
/** /**
* Internal service used by Document List component. * Internal service used by Document List component.
@@ -26,7 +27,7 @@ import { Observable } from 'rxjs/Rx';
@Injectable() @Injectable()
export class SearchService { export class SearchService {
constructor(public authService: AlfrescoAuthenticationService, constructor(public authService: AuthenticationService,
private apiService: AlfrescoApiService) { private apiService: AlfrescoApiService) {
} }

View File

@@ -32,6 +32,7 @@
- [Delete - Show notification message with no permission](#delete---show-notification-message-with-no-permission) - [Delete - Show notification message with no permission](#delete---show-notification-message-with-no-permission)
- [Delete - Disable button checking the permission](#delete---disable-button-checking-the-permission) - [Delete - Disable button checking the permission](#delete---disable-button-checking-the-permission)
- [Download](#download) - [Download](#download)
- [Copy and move](#copy-and-move)
+ [Folder actions](#folder-actions) + [Folder actions](#folder-actions)
* [Context Menu](#context-menu) * [Context Menu](#context-menu)
* [Navigation mode](#navigation-mode) * [Navigation mode](#navigation-mode)
@@ -630,9 +631,12 @@ You can define both folder and document actions at the same time.
<!-- system handler --> <!-- system handler -->
<content-action <content-action
target="folder" icon="content_copy"
title="Delete" target="document"
handler="delete"> title="copy"
permission="update"
[disableWithNoPermission]="true"
handler="copy">
</content-action> </content-action>
<!-- custom handler --> <!-- custom handler -->
@@ -647,6 +651,7 @@ You can define both folder and document actions at the same time.
target="document" target="document"
title="Delete with additional custom callback" title="Delete with additional custom callback"
handler="delete" handler="delete"
permission="delete"
(execute)="myCustomActionAfterDelete($event)"> (execute)="myCustomActionAfterDelete($event)">
</content-action> </content-action>
@@ -679,6 +684,8 @@ All document actions are rendered as a dropdown menu as on the picture below:
The following action handlers are provided out-of-box: The following action handlers are provided out-of-box:
- **Download** (document) - **Download** (document)
- **Copy** (document, folder)
- **Move** (document, folder)
- **Delete** (document, folder) - **Delete** (document, folder)
All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"` All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"`
@@ -766,6 +773,40 @@ Initiates download of the corresponding document file.
![Download document action](docs/assets/document-action-download.png) ![Download document action](docs/assets/document-action-download.png)
##### Copy and move
Shows the destination chooser dialog for copy and move actions
![Copy/move dialog](docs/assets/document-action-copymovedialog.png)
```html
<adf-document-list ...>
<content-actions>
<content-action
icon="content_copy"
target="document"
title="copy"
permission="update"
[disableWithNoPermission]="true"
handler="copy">
</content-action>
<content-action
icon="redo"
target="document"
title="move"
permission="update"
[disableWithNoPermission]="true"
handler="move">
</content-action>
</content-actions>
</adf-document-list>
```
![Copy/move document action](docs/assets/document-action-copymove.png)
#### Folder actions #### Folder actions
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value. You can define system, custom or combined handlers as well just as with the document actions. Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value. You can define system, custom or combined handlers as well just as with the document actions.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -25,6 +25,7 @@ import { ContentActionListComponent } from './src/components/content-action/cont
import { ContentActionComponent } from './src/components/content-action/content-action.component'; import { ContentActionComponent } from './src/components/content-action/content-action.component';
import { ContentColumnListComponent } from './src/components/content-column/content-column-list.component'; import { ContentColumnListComponent } from './src/components/content-column/content-column-list.component';
import { ContentColumnComponent } from './src/components/content-column/content-column.component'; import { ContentColumnComponent } from './src/components/content-column/content-column.component';
import { ContentNodeSelectorComponent } from './src/components/content-node-selector/content-node-selector.component';
import { DocumentListComponent } from './src/components/document-list.component'; import { DocumentListComponent } from './src/components/document-list.component';
import { DocumentMenuActionComponent } from './src/components/document-menu-action.component'; import { DocumentMenuActionComponent } from './src/components/document-menu-action.component';
import { EmptyFolderContentDirective } from './src/components/empty-folder/empty-folder-content.directive'; import { EmptyFolderContentDirective } from './src/components/empty-folder/empty-folder-content.directive';
@@ -34,6 +35,7 @@ import { MaterialModule } from './src/material.module';
import { DocumentActionsService } from './src/services/document-actions.service'; import { DocumentActionsService } from './src/services/document-actions.service';
import { DocumentListService } from './src/services/document-list.service'; import { DocumentListService } from './src/services/document-list.service';
import { FolderActionsService } from './src/services/folder-actions.service'; import { FolderActionsService } from './src/services/folder-actions.service';
import { NodeActionsService } from './src/services/node-actions.service';
// components // components
export * from './src/components/document-list.component'; export * from './src/components/document-list.component';
@@ -71,13 +73,15 @@ export const DOCUMENT_LIST_DIRECTIVES: any[] = [
EmptyFolderContentDirective, EmptyFolderContentDirective,
BreadcrumbComponent, BreadcrumbComponent,
DropdownSitesComponent, DropdownSitesComponent,
DropdownBreadcrumbComponent DropdownBreadcrumbComponent,
ContentNodeSelectorComponent
]; ];
export const DOCUMENT_LIST_PROVIDERS: any[] = [ export const DOCUMENT_LIST_PROVIDERS: any[] = [
DocumentListService, DocumentListService,
FolderActionsService, FolderActionsService,
DocumentActionsService DocumentActionsService,
NodeActionsService
]; ];
@NgModule({ @NgModule({
@@ -92,6 +96,9 @@ export const DOCUMENT_LIST_PROVIDERS: any[] = [
providers: [ providers: [
...DOCUMENT_LIST_PROVIDERS ...DOCUMENT_LIST_PROVIDERS
], ],
entryComponents: [
ContentNodeSelectorComponent
],
exports: [ exports: [
DataTableModule, DataTableModule,
...DOCUMENT_LIST_DIRECTIVES, ...DOCUMENT_LIST_DIRECTIVES,

View File

@@ -17,12 +17,13 @@
import { EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { AlfrescoContentService, CoreModule } from 'ng2-alfresco-core'; import { AlfrescoContentService, AlfrescoTranslationService, CoreModule, NotificationService } from 'ng2-alfresco-core';
import { FileNode } from './../../assets/document-library.model.mock'; import { FileNode } from './../../assets/document-library.model.mock';
import { DocumentListServiceMock } from './../../assets/document-list.service.mock'; import { DocumentListServiceMock } from './../../assets/document-list.service.mock';
import { ContentActionHandler } from './../../models/content-action.model'; import { ContentActionHandler } from './../../models/content-action.model';
import { DocumentActionsService } from './../../services/document-actions.service'; import { DocumentActionsService } from './../../services/document-actions.service';
import { FolderActionsService } from './../../services/folder-actions.service'; import { FolderActionsService } from './../../services/folder-actions.service';
import { NodeActionsService } from './../../services/node-actions.service';
import { DocumentListComponent } from './../document-list.component'; import { DocumentListComponent } from './../document-list.component';
import { ContentActionListComponent } from './content-action-list.component'; import { ContentActionListComponent } from './content-action-list.component';
import { ContentActionComponent } from './content-action.component'; import { ContentActionComponent } from './content-action.component';
@@ -35,6 +36,9 @@ describe('ContentAction', () => {
let folderActions: FolderActionsService; let folderActions: FolderActionsService;
let contentService: AlfrescoContentService; let contentService: AlfrescoContentService;
let translateService: AlfrescoTranslationService;
let notificationService: NotificationService;
let nodeActionsService: NodeActionsService;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -49,9 +53,12 @@ describe('ContentAction', () => {
beforeEach(() => { beforeEach(() => {
contentService = TestBed.get(AlfrescoContentService); contentService = TestBed.get(AlfrescoContentService);
translateService = <AlfrescoTranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, translateService, null, null);
notificationService = new NotificationService(null);
let documentServiceMock = new DocumentListServiceMock(); let documentServiceMock = new DocumentListServiceMock();
documentActions = new DocumentActionsService(null, null); documentActions = new DocumentActionsService(translateService, notificationService, nodeActionsService);
folderActions = new FolderActionsService(null, contentService); folderActions = new FolderActionsService(translateService, notificationService, nodeActionsService, null, contentService);
documentList = new DocumentListComponent(documentServiceMock, null, null, null); documentList = new DocumentListComponent(documentServiceMock, null, null, null);
actionList = new ContentActionListComponent(documentList); actionList = new ContentActionListComponent(documentList);

View File

@@ -0,0 +1,73 @@
<header mdDialogTitle
class="adf-content-node-selector-title"
data-automation-id="content-node-selector-title">{{title}}</header>
<section mdDialogContent
class="adf-content-node-selector-content"
(node-select)="onNodeSelect($event)"
(node-unselect)="onNodeUnselect($event)">
<md-input-container floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input #searchInput
mdInput
placeholder="Search"
(keyup)="search(searchInput.value)"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
<md-icon *ngIf="searchTerm.length > 0"
mdSuffix (click)="clear()"
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-clear">clear</md-icon>
<md-icon *ngIf="searchTerm.length === 0"
mdSuffix
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-icon">search</md-icon>
</md-input-container>
<adf-sites-dropdown
(change)="siteChanged($event)"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
<div class="adf-content-node-selector-content-list" data-automation-id="content-node-selector-content-list">
<adf-document-list *ngIf="searched"
#documentList
[node]="nodes"
[permissionsStyle]="permissionsStyle"
[creationMenuActions]="false"
[currentFolderId]="currentFolderId"
[selectionMode]="'single'"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
[enablePagination]="false"
data-automation-id="content-node-selector-document-list">
<empty-folder-content>
<template>
<div>{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}</div>
</template>
</empty-folder-content>
</adf-document-list>
</div>
</section>
<footer mdDialogActions class="adf-content-node-selector-actions">
<button *ngIf="inDialog"
md-button
class="adf-content-node-selector-actions-cancel"
(click)="close()"
data-automation-id="content-node-selector-actions-cancel">{{ 'NODE_SELECTOR.CANCEL' | translate }}
</button>
<button md-button
[disabled]="!chosenNode"
class="adf-content-node-selector-actions-choose"
(click)="choose()"
data-automation-id="content-node-selector-actions-choose">{{ 'NODE_SELECTOR.CHOOSE' | translate }}
</button>
</footer>

View File

@@ -0,0 +1,116 @@
@import 'theming';
.#{$ADF}-content-node-selector-dialog {
.mat-dialog-container {
padding: 0;
}
.#{$ADF}-content-node-selector {
&-title,
&-content,
&-actions {
padding: 16px;
margin: 0;
}
&-title {
text-transform: capitalize;
}
&-content {
padding-top: 0;
&-input {
width: 100%;
&-icon {
color: rgba(0, 0, 0, 0.38);
cursor: pointer;
&:hover {
color: rgba(0, 0, 0, 1);
}
}
}
& /deep/ .mat-input-underline .mat-input-ripple {
height: 1px;
transition: none;
}
& /deep/ .adf-site-dropdown-list-element {
width: 100%;
margin-bottom: 20px;
.mat-select-trigger {
font-size: 14px;
}
.mat-select-placeholder,
&.mat-select {
font-family: 'Muli', "Helvetica", "Arial", sans-serif;
}
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.07);
& /deep/ .adf-data-table {
border: none;
.adf-no-content-container {
text-align: center;
}
thead {
display: none;
}
.adf-data-table-cell {
padding-top: 8px;
padding-bottom: 8px;
border-top: none;
height: 30px;
}
tbody tr {
height: auto !important;
&:last-child {
.adf-data-table-cell {
border-bottom: none;
}
}
}
}
}
}
&-actions {
padding: 8px;
background-color: rgb(250, 250, 250);
display: flex;
justify-content: flex-end;
color: rgb(121, 121, 121);
&:last-child {
margin-bottom: 0px;
}
&-cancel {
font-weight: normal;
}
&-choose {
font-weight: normal;
&[disabled] {
opacity: 0.6;
}
}
}
}
}

View File

@@ -0,0 +1,377 @@
/*!
* @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 { DebugElement, EventEmitter } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MD_DIALOG_DATA, MdDialogRef } from '@angular/material';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity, NodePaging } from 'alfresco-js-api';
import { 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 { DocumentListComponent } from '../document-list.component';
import { DocumentMenuActionComponent } from '../document-menu-action.component';
import { EmptyFolderContentDirective } from '../empty-folder/empty-folder-content.directive';
import { DropdownSitesComponent } from '../site-dropdown/sites-dropdown.component';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
const ONE_FOLDER_RESULT = {
list: {
entries: [
{
entry: {
id: '123', name: 'MyFolder', isFile: false, isFolder: true,
createdByUser: { displayName: 'John Doe' },
modifiedByUser: { displayName: 'John Doe' }
}
}
]
}
};
const NO_RESULT = {
list: {
entries: []
}
};
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorComponent;
let fixture: ComponentFixture<ContentNodeSelectorComponent>;
let element: DebugElement;
let data: any;
let searchService: SearchService;
let searchSpy: jasmine.Spy;
let _resolve: Function;
let _reject: Function;
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', {});
fixture.detectChanges();
}
function respondWithSearchResults(result) {
_resolve(result);
}
function setupTestbed(plusProviders) {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot(),
DataTableModule.forRoot(),
MaterialModule
],
declarations: [
DocumentListComponent,
DocumentMenuActionComponent,
EmptyFolderContentDirective,
DropdownSitesComponent,
ContentNodeSelectorComponent
],
providers: [
AlfrescoTranslationService,
DocumentListService,
SearchService,
...plusProviders
]
});
}
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Dialog features', () => {
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
select: new EventEmitter<MinimalNodeEntryEntity>()
};
setupTestbed([{ provide: MD_DIALOG_DATA, useValue: data }]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
element = fixture.debugElement;
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('Data injecting with the "Material dialog way"', () => {
it('should show the INJECTED title', () => {
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should trigger the INJECTED select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
data.select.subscribe((node) => {
expect(node).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
});
describe('Cancel button', () => {
let dummyMdDialogRef;
beforeEach(() => {
dummyMdDialogRef = <MdDialogRef<ContentNodeSelectorComponent>> { close: () => {} };
});
it('should be shown if dialogRef is injected', () => {
const componentInstance = new ContentNodeSelectorComponent(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);
componentInstance.close();
expect(dummyMdDialogRef.close).toHaveBeenCalled();
});
});
});
describe('General component features', () => {
beforeEach(async(() => {
setupTestbed([]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
element = fixture.debugElement;
component = fixture.componentInstance;
searchService = TestBed.get(SearchService);
searchSpy = spyOn(searchService, 'getQueryNodesPromise').and.callFake(() => {
return new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
});
});
describe('Parameters', () => {
it('should show the title', () => {
component.title = 'Move along citizen...';
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
component.select.subscribe((node) => {
expect(node).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
});
describe('Search functionality', () => {
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
});
});
it('should NOT call the search api if the searchTerm length is less than 4 characters', () => {
typeToSearchBox('1');
typeToSearchBox('12');
typeToSearchBox('123');
expect(searchSpy).not.toHaveBeenCalled();
});
xit('should debounce the search call by 500 ms', () => {
});
it('should call the search api on changing the site selectbox\'s value', () => {
typeToSearchBox('vegeta');
expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search');
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
}]);
});
it('should show the search icon by default without the X (clear) icon', () => {
fixture.detectChanges();
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).not.toBeNull('Search icon should be in the DOM');
expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM');
});
it('should show the X (clear) icon without the search icon when the search contains at least one character', () => {
fixture.detectChanges();
typeToSearchBox('123');
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).toBeNull('Search icon should NOT be in the DOM');
expect(clearIcon).not.toBeNull('Clear icon should be in the DOM');
});
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.clear();
expect(component.searched).toBe(false);
expect(component.searchTerm).toBe('');
expect(component.nodes).toEqual([]);
expect(component.chosenNode).toBeNull();
});
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 result list when search was performed', async(() => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
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');
});
}));
it('should show the default text instead of result list if search was cleared', async(() => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(clearButton).not.toBeNull('Clear button should be in DOM');
clearButton.triggerEventHandler('click', {});
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');
});
}));
xit('should do something with pagination or with many results', () => {
});
xit('should trigger some kind of error when error happened during search', () => {
});
});
describe('Cancel button', () => {
it('should not be shown if dialogRef is NOT injected', () => {
const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]'));
expect(closeButton).toBeNull();
});
});
describe('Choose button', () => {
it('should be disabled by default', () => {
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 enabled when clicking on one element in the list (onNodeSelect)', () => {
fixture.detectChanges();
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
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)', () => {
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
fixture.detectChanges();
component.onNodeUnselect();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
});
describe('Mini integration test', () => {
xit('should trigger the select event properly when search results are loaded, one element is selected and choose button is clicked', () => {
});
});
});
});

View File

@@ -0,0 +1,152 @@
/*!
* @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, EventEmitter, Inject, Input, Optional, Output, 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';
export interface ContentNodeSelectorComponentData {
title: string;
select: EventEmitter<MinimalNodeEntryEntity>;
}
@Component({
selector: 'adf-content-node-selector',
styleUrls: ['./content-node-selector.component.scss'],
templateUrl: './content-node-selector.component.html',
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorComponent {
nodes: NodePaging|Array<any>;
siteId: null|string;
searchTerm: string = '';
searched: boolean = false;
inDialog: boolean = false;
chosenNode: MinimalNodeEntryEntity | null = null;
@Input()
title: string;
@Output()
select: EventEmitter<MinimalNodeEntryEntity> = new EventEmitter<MinimalNodeEntryEntity>();
constructor(private searchService: SearchService,
@Optional() private translateService: AlfrescoTranslationService,
@Optional() @Inject(MD_DIALOG_DATA) public data?: ContentNodeSelectorComponentData,
@Optional() private containingDialog?: MdDialogRef<ContentNodeSelectorComponent>) {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist');
}
if (data) {
this.title = data.title;
this.select = data.select;
}
if (containingDialog) {
this.inDialog = true;
}
}
/**
* Updates the site attribute and starts a new search
*
* @param chosenSite Sitemodel to search within
*/
siteChanged(chosenSite: SiteModel): void {
this.siteId = chosenSite.guid;
this.querySearch();
}
/**
* Updates the searchTerm attribute and starts a new search
*
* @param searchTerm string value to search against
*/
search(searchTerm: string): void {
this.searchTerm = searchTerm;
this.querySearch();
}
/**
* Clear the search input
*/
clear(): void {
this.searched = false;
this.searchTerm = '';
this.nodes = [];
this.chosenNode = null;
}
/**
* Perform the call to searchService with the proper parameters
*/
private querySearch(): void {
if (this.searchTerm.length > 3) {
const searchTerm = this.searchTerm + '*';
let searchOpts: SearchOptions = {
include: ['path'],
skipCount: 0,
rootNodeId: this.siteId,
nodeType: 'cm:folder',
maxItems: 40,
orderBy: null
};
this.searchService
.getNodeQueryResults(searchTerm, searchOpts)
.subscribe(
results => {
this.searched = true;
this.nodes = results;
}
);
}
}
/**
* Invoked when user selects a node
*
* @param event CustomEvent for node-select
*/
onNodeSelect(event: any): void {
this.chosenNode = event.detail.node.entry;
}
/**
* * Invoked when user unselects a node
*/
onNodeUnselect(): void {
this.chosenNode = null;
}
/**
* Emit event with the chosen node
*/
choose(): void {
this.select.next(this.chosenNode);
}
/**
* Close the dialog
*/
close(): void {
this.containingDialog.close();
}
}

View File

@@ -1,6 +1,7 @@
<div id="site-dropdown-container" class="adf-site-dropdown-container"> <div id="site-dropdown-container" class="adf-site-dropdown-container">
<md-select class="adf-site-dropdown-list-element" id="site-dropdown" <md-select class="adf-site-dropdown-list-element" id="site-dropdown"
placeholder="{{'DROPDOWN.PLACEHOLDER_LABEL' | translate}}" placeholder="{{'DROPDOWN.PLACEHOLDER_LABEL' | translate}}"
floatPlaceholder="never"
[(ngModel)]="siteSelected" [(ngModel)]="siteSelected"
(ngModelChange)="selectedSite()"> (ngModelChange)="selectedSite()">
<md-option id="default_site_option" [value]="DEFAULT_VALUE">{{'DROPDOWN.DEFAULT_OPTION' | translate}}</md-option> <md-option id="default_site_option" [value]="DEFAULT_VALUE">{{'DROPDOWN.DEFAULT_OPTION' | translate}}</md-option>

View File

@@ -1,10 +1,7 @@
@import 'theming'; @import 'theming';
.adf-site-dropdown { .adf-site-dropdown {
&-list-element { &-list-element {
width: 300px; width: 300px;
} }
} }

View File

@@ -15,5 +15,26 @@
"DROPDOWN": { "DROPDOWN": {
"PLACEHOLDER_LABEL": "Site List", "PLACEHOLDER_LABEL": "Site List",
"DEFAULT_OPTION": "No Site Chosen" "DEFAULT_OPTION": "No Site Chosen"
},
"NODE_SELECTOR": {
"CANCEL": "Cancel",
"CHOOSE": "Choose",
"NO_RESULTS": "No results found"
},
"OPERATION": {
"SUCCES": {
"CONTENT": {
"COPY": "Content was copied successfully.",
"MOVE": "Content was moved successfully."
},
"FOLDER": {
"COPY": "Folder was copied successfully.",
"MOVE": "Folder was moved successfully."
}
},
"ERROR": {
"CONFLICT": "Name already exists in target location.",
"UNKNOWN": "Unknown error happened."
}
} }
} }

View File

@@ -16,15 +16,18 @@
*/ */
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { MdButtonModule, MdIconModule, MdMenuModule, MdProgressSpinnerModule, MdSelectModule } from '@angular/material'; import { MdButtonModule, MdDialogModule, MdIconModule, MdInputModule, MdMenuModule, MdProgressSpinnerModule, MdRippleModule, MdSelectModule } from '@angular/material';
export function modules() { export function modules() {
return [ return [
MdMenuModule, MdMenuModule,
MdDialogModule,
MdButtonModule, MdButtonModule,
MdIconModule, MdIconModule,
MdInputModule,
MdProgressSpinnerModule, MdProgressSpinnerModule,
MdSelectModule MdSelectModule,
MdRippleModule
]; ];
} }

View File

@@ -15,23 +15,32 @@
* limitations under the License. * limitations under the License.
*/ */
import { AlfrescoContentService } from 'ng2-alfresco-core'; import { MdDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { AlfrescoContentService, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { FileNode, FolderNode } from '../assets/document-library.model.mock'; import { FileNode, FolderNode } from '../assets/document-library.model.mock';
import { DocumentListServiceMock } from '../assets/document-list.service.mock'; import { DocumentListServiceMock } from '../assets/document-list.service.mock';
import { ContentActionHandler } from '../models/content-action.model'; import { ContentActionHandler } from '../models/content-action.model';
import { DocumentActionsService } from './document-actions.service'; import { DocumentActionsService } from './document-actions.service';
import { DocumentListService } from './document-list.service'; import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
describe('DocumentActionsService', () => { describe('DocumentActionsService', () => {
let service: DocumentActionsService; let service: DocumentActionsService;
let documentListService: DocumentListService; let documentListService: DocumentListService;
let contentService: AlfrescoContentService; let contentService: AlfrescoContentService;
let translateService: AlfrescoTranslationService;
let notificationService: NotificationService;
let nodeActionsService: NodeActionsService;
beforeEach(() => { beforeEach(() => {
documentListService = new DocumentListServiceMock(); documentListService = new DocumentListServiceMock();
contentService = new AlfrescoContentService(null, null, null); contentService = new AlfrescoContentService(null, null, null);
service = new DocumentActionsService(documentListService, contentService); translateService = <AlfrescoTranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, translateService, null, null);
notificationService = new NotificationService(null);
service = new DocumentActionsService(translateService, notificationService, nodeActionsService, documentListService, contentService);
}); });
it('should register default download action', () => { it('should register default download action', () => {
@@ -63,7 +72,7 @@ describe('DocumentActionsService', () => {
let file = new FileNode(); let file = new FileNode();
expect(service.canExecuteAction(file)).toBeTruthy(); expect(service.canExecuteAction(file)).toBeTruthy();
service = new DocumentActionsService(null); service = new DocumentActionsService(translateService, notificationService, nodeActionsService);
expect(service.canExecuteAction(file)).toBeFalsy(); expect(service.canExecuteAction(file)).toBeFalsy();
}); });
@@ -80,23 +89,6 @@ describe('DocumentActionsService', () => {
expect(service.setHandler('my-handler', handler)).toBeTruthy(); expect(service.setHandler('my-handler', handler)).toBeTruthy();
}); });
// TODO: to be removed once demo handlers are removed
it('should execute demo actions', () => {
spyOn(window, 'alert').and.stub();
service.getHandler('system1')(null);
expect(window.alert).toHaveBeenCalledWith('standard document action 1');
service.getHandler('system2')(null);
expect(window.alert).toHaveBeenCalledWith('standard document action 2');
});
// TODO: to be removed once demo handlers are removed
it('should register demo handlers', () => {
expect(service.getHandler('system1')).toBeDefined();
expect(service.getHandler('system2')).toBeDefined();
});
it('should register delete action', () => { it('should register delete action', () => {
expect(service.getHandler('delete')).toBeDefined(); expect(service.getHandler('delete')).toBeDefined();
}); });
@@ -201,7 +193,7 @@ describe('DocumentActionsService', () => {
}); });
it('should require internal service for download action', () => { it('should require internal service for download action', () => {
let actionService = new DocumentActionsService(null, contentService); let actionService = new DocumentActionsService(translateService, notificationService, nodeActionsService, null, contentService);
let file = new FileNode(); let file = new FileNode();
let result = actionService.getHandler('download')(file); let result = actionService.getHandler('download')(file);
result.subscribe((value) => { result.subscribe((value) => {
@@ -210,7 +202,7 @@ describe('DocumentActionsService', () => {
}); });
it('should require content service for download action', () => { it('should require content service for download action', () => {
let actionService = new DocumentActionsService(documentListService, null); let actionService = new DocumentActionsService(translateService, notificationService, nodeActionsService, documentListService, null);
let file = new FileNode(); let file = new FileNode();
let result = actionService.getHandler('download')(file); let result = actionService.getHandler('download')(file);
result.subscribe((value) => { result.subscribe((value) => {

View File

@@ -16,12 +16,15 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AlfrescoContentService } from 'ng2-alfresco-core'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { ContentNodeSelectorComponent } from '../components/content-node-selector/content-node-selector.component';
import { ContentActionHandler } from '../models/content-action.model'; import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model'; import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service'; import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
@Injectable() @Injectable()
export class DocumentActionsService { export class DocumentActionsService {
@@ -30,9 +33,15 @@ export class DocumentActionsService {
private handlers: { [id: string]: ContentActionHandler; } = {}; private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private documentListService?: DocumentListService, constructor(private translateService: AlfrescoTranslationService,
private notificationService: NotificationService,
private nodeActionsService: NodeActionsService,
private documentListService?: DocumentListService,
private contentService?: AlfrescoContentService) { private contentService?: AlfrescoContentService) {
this.setupActionHandlers(); this.setupActionHandlers();
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist');
}
} }
getHandler(key: string): ContentActionHandler { getHandler(key: string): ContentActionHandler {
@@ -58,35 +67,9 @@ export class DocumentActionsService {
private setupActionHandlers() { private setupActionHandlers() {
this.handlers['download'] = this.download.bind(this); this.handlers['download'] = this.download.bind(this);
this.handlers['copy'] = this.copyNode.bind(this);
this.handlers['move'] = this.moveNode.bind(this);
this.handlers['delete'] = this.deleteNode.bind(this); this.handlers['delete'] = this.deleteNode.bind(this);
// TODO: for demo purposes only, will be removed during future revisions
this.handlers['system1'] = this.handleStandardAction1.bind(this);
this.handlers['system2'] = this.handleStandardAction2.bind(this);
}
// TODO: for demo purposes only, will be removed during future revisions
/**
* @deprecated in 1.7.0
*
* @private
* @memberof DocumentActionsService
*/
private handleStandardAction1(/*obj: any*/) {
console.log('handleStandardAction1 is deprecated in 1.7.0 and will be removed in future versions');
window.alert('standard document action 1');
}
// TODO: for demo purposes only, will be removed during future revisions
/**
* @deprecated in 1.7.0
*
* @private
* @memberof DocumentActionsService
*/
private handleStandardAction2(/*obj: any*/) {
console.log('handleStandardAction2 is deprecated in 1.7.0 and will be removed in future versions');
window.alert('standard document action 2');
} }
private download(obj: any): Observable<boolean> { private download(obj: any): Observable<boolean> {
@@ -102,6 +85,43 @@ export class DocumentActionsService {
return Observable.of(false); return Observable.of(false);
} }
private copyNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.copyContent(obj.entry, permission);
this.prepareHandlers(actionObservable, 'content', 'copy', target, permission);
return actionObservable;
}
private moveNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.moveContent(obj.entry, permission);
this.prepareHandlers(actionObservable, 'content', 'move', target, permission);
return actionObservable;
}
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();
}
},
(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);
}
}
);
}
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> { private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {
let handlerObservable; let handlerObservable;

View File

@@ -177,4 +177,24 @@ describe('DocumentListService', () => {
contentType: 'json' contentType: 'json'
}); });
}); });
it('should copy a node', (done) => {
service.copyNode('node-id', 'parent-id').subscribe(done);
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
expect(jasmine.Ajax.requests.mostRecent().url).toContain('/nodes/node-id/copy');
expect(jasmine.Ajax.requests.mostRecent().params).toEqual(JSON.stringify({ targetParentId: 'parent-id' }));
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json' });
});
it('should move a node', (done) => {
service.moveNode('node-id', 'parent-id').subscribe(done);
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
expect(jasmine.Ajax.requests.mostRecent().url).toContain('/nodes/node-id/move');
expect(jasmine.Ajax.requests.mostRecent().params).toEqual(JSON.stringify({ targetParentId: 'parent-id' }));
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json' });
});
}); });

View File

@@ -65,6 +65,28 @@ export class DocumentListService {
return Observable.fromPromise(this.apiService.getInstance().nodes.deleteNode(nodeId)); return Observable.fromPromise(this.apiService.getInstance().nodes.deleteNode(nodeId));
} }
/**
* Copy a node to destination node
*
* @param nodeId The id of the node to be copied
* @param targetParentId The id of the folder-node where the node have to be copied to
*/
copyNode(nodeId: string, targetParentId: string) {
return Observable.fromPromise(this.apiService.getInstance().nodes.copyNode(nodeId, { targetParentId }))
.catch(err => this.handleError(err));
}
/**
* Move a node to destination node
*
* @param nodeId The id of the node to be moved
* @param targetParentId The id of the folder-node where the node have to be moved to
*/
moveNode(nodeId: string, targetParentId: string) {
return Observable.fromPromise(this.apiService.getInstance().nodes.moveNode(nodeId, { targetParentId }))
.catch(err => this.handleError(err));
}
/** /**
* Create a new folder in the path. * Create a new folder in the path.
* @param name Folder name * @param name Folder name

View File

@@ -16,12 +16,13 @@
*/ */
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { AppConfigModule, CoreModule } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, AppConfigModule, CoreModule, NotificationService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { FileNode, FolderNode } from '../assets/document-library.model.mock'; import { FileNode, FolderNode } from '../assets/document-library.model.mock';
import { ContentActionHandler } from '../models/content-action.model'; import { ContentActionHandler } from '../models/content-action.model';
import { DocumentListService } from './document-list.service'; import { DocumentListService } from './document-list.service';
import { FolderActionsService } from './folder-actions.service'; import { FolderActionsService } from './folder-actions.service';
import { NodeActionsService } from './node-actions.service';
describe('FolderActionsService', () => { describe('FolderActionsService', () => {
@@ -38,7 +39,10 @@ describe('FolderActionsService', () => {
], ],
providers: [ providers: [
DocumentListService, DocumentListService,
FolderActionsService FolderActionsService,
NodeActionsService,
AlfrescoTranslationService,
NotificationService
] ]
}).compileComponents(); }).compileComponents();
})); }));
@@ -90,23 +94,6 @@ describe('FolderActionsService', () => {
expect(service.setHandler('my-handler', handler)).toBeTruthy(); expect(service.setHandler('my-handler', handler)).toBeTruthy();
}); });
// TODO: to be removed once demo handlers are removed
it('should execute demo actions', () => {
spyOn(window, 'alert').and.stub();
service.getHandler('system1')(null);
expect(window.alert).toHaveBeenCalledWith('standard folder action 1');
service.getHandler('system2')(null);
expect(window.alert).toHaveBeenCalledWith('standard folder action 2');
});
// TODO: to be removed once demo handlers are removed
it('should register demo handlers', () => {
expect(service.getHandler('system1')).toBeDefined();
expect(service.getHandler('system2')).toBeDefined();
});
it('should register delete action', () => { it('should register delete action', () => {
expect(service.getHandler('delete')).toBeDefined(); expect(service.getHandler('delete')).toBeDefined();
}); });

View File

@@ -16,11 +16,13 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AlfrescoContentService } from 'ng2-alfresco-core'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { Observable, Subject } from 'rxjs/Rx'; import { Observable, Subject } from 'rxjs/Rx';
import { ContentActionHandler } from '../models/content-action.model'; import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model'; import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service'; import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
@Injectable() @Injectable()
export class FolderActionsService { export class FolderActionsService {
@@ -29,7 +31,10 @@ export class FolderActionsService {
private handlers: { [id: string]: ContentActionHandler; } = {}; private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private documentListService: DocumentListService, constructor(private translateService: AlfrescoTranslationService,
private notificationService: NotificationService,
private nodeActionsService: NodeActionsService,
private documentListService: DocumentListService,
private contentService: AlfrescoContentService) { private contentService: AlfrescoContentService) {
this.setupActionHandlers(); this.setupActionHandlers();
} }
@@ -56,36 +61,46 @@ export class FolderActionsService {
} }
private setupActionHandlers() { private setupActionHandlers() {
this.handlers['copy'] = this.copyNode.bind(this);
this.handlers['move'] = this.moveNode.bind(this);
this.handlers['delete'] = this.deleteNode.bind(this); this.handlers['delete'] = this.deleteNode.bind(this);
// TODO: for demo purposes only, will be removed during future revisions
this.handlers['system1'] = this.handleStandardAction1.bind(this);
this.handlers['system2'] = this.handleStandardAction2.bind(this);
} }
// TODO: for demo purposes only, will be removed during future revisions private copyNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
/** const actionObservable = this.nodeActionsService.copyFolder(obj.entry, permission);
* @deprecated in 1.7.0 this.prepareHandlers(actionObservable, 'folder', 'copy', target, permission);
* return actionObservable;
* @private
* @param {*} document
* @memberof FolderActionsService
*/
private handleStandardAction1(/*document: any*/) {
console.log('handleStandardAction1 is deprecated in 1.7.0 and will be removed in future versions');
window.alert('standard folder action 1');
} }
// TODO: for demo purposes only, will be removed during future revisions private moveNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
/** const actionObservable = this.nodeActionsService.moveFolder(obj.entry, permission);
* @deprecated in 1.7.0 this.prepareHandlers(actionObservable, 'folder', 'move', target, permission);
* return actionObservable;
* @private }
* @memberof FolderActionsService
*/ private prepareHandlers(actionObservable, type: string, action: string, target?: any, permission?: string): void {
private handleStandardAction2(/*document: any*/) { actionObservable.subscribe(
console.log('handleStandardAction1 is deprecated in 1.7.0 and will be removed in future versions'); (fileOperationMessage) => {
window.alert('standard folder action 2'); this.notificationService.openSnackMessage(fileOperationMessage, 3000);
if (target && typeof target.reload === 'function') {
target.reload();
}
},
(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);
}
}
);
} }
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> { private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {

View File

@@ -0,0 +1,121 @@
/*!
* @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 { EventEmitter, Injectable } from '@angular/core';
import { MdDialog } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoContentService, AlfrescoTranslationService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Rx';
import { ContentNodeSelectorComponent } from '../components/content-node-selector/content-node-selector.component';
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');
}
}
/**
* Copy content node
*
* @param contentEntry node to copy
* @param permission permission which is needed to apply the action
*/
public copyContent(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('copy', 'content', contentEntry, permission);
}
/**
* Copy folder node
*
* @param contentEntry node to copy
* @param permission permission which is needed to apply the action
*/
public copyFolder(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('copy', 'folder', contentEntry, permission);
}
/**
* Move content node
*
* @param contentEntry node to move
* @param permission permission which is needed to apply the action
*/
public moveContent(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('move', 'content', contentEntry, permission);
}
/**
* Move folder node
*
* @param contentEntry node to move
* @param permission permission which is needed to apply the action
*/
public moveFolder(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('move', 'folder', contentEntry, permission);
}
/**
* General method for performing the given operation (copy|move)
*
* @param action the action to perform (copy|move)
* @param type type of the content (content|folder)
* @param contentEntry the contentEntry which has to have the action performed on
* @param permission permission which is needed to apply the action
*/
private doFileOperation(action: string, type: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
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>();
this.dialog.open(ContentNodeSelectorComponent, {
data: { title, select },
panelClass: 'adf-content-node-selector-dialog',
width: '576px'
});
select.subscribe((parent: MinimalNodeEntryEntity) => {
this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, parent.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);
}
);
this.dialog.closeAll();
});
return observable;
} else {
observable.error(403);
return observable;
}
}
}

View File

@@ -17,25 +17,24 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CoreModule } from 'ng2-alfresco-core'; import { CoreModule, SearchService } from 'ng2-alfresco-core';
import { DocumentListModule } from 'ng2-alfresco-documentlist'; import { DocumentListModule } from 'ng2-alfresco-documentlist';
import { SearchAutocompleteComponent } from './src/components/search-autocomplete.component'; import { SearchAutocompleteComponent } from './src/components/search-autocomplete.component';
import { SearchControlComponent } from './src/components/search-control.component'; import { SearchControlComponent } from './src/components/search-control.component';
import { SearchComponent } from './src/components/search.component'; import { SearchComponent } from './src/components/search.component';
import { SearchService } from './src/services/search.service';
// services // services
export * from './src/services/search.service'; export { SearchOptions, SearchService } from 'ng2-alfresco-core';
export * from './src/components/search.component'; export * from './src/components/search.component';
export * from './src/components/search-control.component'; export * from './src/components/search-control.component';
export * from './src/components/search-autocomplete.component'; export * from './src/components/search-autocomplete.component';
// Old Deprecated export // Old Deprecated export
import { SearchService as AlfrescoSearchService } from 'ng2-alfresco-core';
import { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component'; import { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component';
import { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component'; import { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component';
import { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component'; import { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component';
import { SearchService as AlfrescoSearchService } from './src/services/search.service'; export { SearchService as AlfrescoSearchService } from 'ng2-alfresco-core';
export { SearchService as AlfrescoSearchService } from './src/services/search.service';
export { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component'; export { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component';
export { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component'; export { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component';
export { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component'; export { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component';

View File

@@ -23,9 +23,9 @@ import {
AlfrescoContentService, AlfrescoContentService,
AlfrescoSettingsService, AlfrescoSettingsService,
AlfrescoTranslationService, AlfrescoTranslationService,
CoreModule CoreModule,
SearchService
} from 'ng2-alfresco-core'; } from 'ng2-alfresco-core';
import { SearchService } from '../services/search.service';
import { errorJson, folderResult, noResult, result, results } from './../assets/search.component.mock'; import { errorJson, folderResult, noResult, result, results } from './../assets/search.component.mock';
import { TranslationMock } from './../assets/translation.service.mock'; import { TranslationMock } from './../assets/translation.service.mock';
import { SearchAutocompleteComponent } from './search-autocomplete.component'; import { SearchAutocompleteComponent } from './search-autocomplete.component';

View File

@@ -17,9 +17,8 @@
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoTranslationService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, SearchOptions, SearchService } from 'ng2-alfresco-core';
import { ThumbnailService } from 'ng2-alfresco-core'; import { ThumbnailService } from 'ng2-alfresco-core';
import { SearchOptions, SearchService } from './../services/search.service';
declare var require: any; declare var require: any;

View File

@@ -17,8 +17,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ThumbnailService } from 'ng2-alfresco-core'; import { ThumbnailService } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, CoreModule, SearchService } from 'ng2-alfresco-core';
import { SearchService } from '../services/search.service';
import { result } from './../assets/search.component.mock'; import { result } from './../assets/search.component.mock';
import { TranslationMock } from './../assets/translation.service.mock'; import { TranslationMock } from './../assets/translation.service.mock';
import { SearchAutocompleteComponent } from './search-autocomplete.component'; import { SearchAutocompleteComponent } from './search-autocomplete.component';

View File

@@ -18,11 +18,10 @@
import { DebugElement, ReflectiveInjector, SimpleChange } from '@angular/core'; import { DebugElement, ReflectiveInjector, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AlfrescoTranslationService, CoreModule, NotificationService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, CoreModule, NotificationService, SearchService } from 'ng2-alfresco-core';
import { DocumentListModule } from 'ng2-alfresco-documentlist'; import { DocumentListModule } from 'ng2-alfresco-documentlist';
import { PermissionModel } from 'ng2-alfresco-documentlist'; import { PermissionModel } from 'ng2-alfresco-documentlist';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { SearchService } from '../services/search.service';
import { TranslationMock } from './../assets/translation.service.mock'; import { TranslationMock } from './../assets/translation.service.mock';
import { SearchComponent } from './search.component'; import { SearchComponent } from './search.component';
@@ -242,7 +241,7 @@ describe('SearchComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
let errorEl = element.querySelector('[data-automation-id="search_error_message"]'); let errorEl = element.querySelector('[data-automation-id="search_error_message"]');
expect(errorEl).not.toBeNull(); expect(errorEl).not.toBeNull();
expect((<any>errorEl).innerText).toBe('SEARCH.RESULTS.ERROR'); expect((<any> errorEl).innerText).toBe('SEARCH.RESULTS.ERROR');
done(); done();
}); });

View File

@@ -18,9 +18,8 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output, SimpleChanges } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { NodePaging, Pagination } from 'alfresco-js-api'; import { NodePaging, Pagination } from 'alfresco-js-api';
import { AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, NotificationService, SearchOptions, SearchService } from 'ng2-alfresco-core';
import { PermissionModel } from 'ng2-alfresco-documentlist'; import { PermissionModel } from 'ng2-alfresco-documentlist';
import { SearchOptions, SearchService } from './../services/search.service';
@Component({ @Component({
selector: 'adf-search, alfresco-search', selector: 'adf-search, alfresco-search',