diff --git a/.angular-cli.json b/.angular-cli.json
index a446cd5b7..6cdb561bf 100644
--- a/.angular-cli.json
+++ b/.angular-cli.json
@@ -30,7 +30,7 @@
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
- "styles.css"
+ "styles.scss"
],
"scripts": [
"../node_modules/pdfjs-dist/build/pdf.js",
@@ -41,6 +41,11 @@
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
+ },
+ "stylePreprocessorOptions": {
+ "includePaths": [
+ "app/ui"
+ ]
}
}
],
diff --git a/.editorconfig b/.editorconfig
index 6e87a003d..9b7352176 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,7 +4,7 @@ root = true
[*]
charset = utf-8
indent_style = space
-indent_size = 2
+indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
diff --git a/src/app/adf.module.ts b/src/app/adf.module.ts
index 2034755b0..ffa572b7d 100644
--- a/src/app/adf.module.ts
+++ b/src/app/adf.module.ts
@@ -10,20 +10,19 @@ import { UploadModule } from 'ng2-alfresco-upload';
import { SearchModule } from 'ng2-alfresco-search';
export function modules() {
- return [
- // ADF modules
- CoreModule,
- DataTableModule,
- DocumentListModule,
- LoginModule,
- SearchModule,
- UploadModule,
- ViewerModule
- ];
+ return [
+ CoreModule,
+ DataTableModule,
+ DocumentListModule,
+ LoginModule,
+ SearchModule,
+ UploadModule,
+ ViewerModule
+ ];
}
@NgModule({
- imports: modules(),
- exports: modules()
+ imports: modules(),
+ exports: modules()
})
-export class AdfModule {}
+export class AdfModule { }
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index e69de29bb..2453e2916 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -0,0 +1,4 @@
+:host {
+ display: flex;
+ flex: 1;
+}
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index bcbdf36b3..bae855fbc 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -1,27 +1,27 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
- }));
- it('should create the app', async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.debugElement.componentInstance;
- expect(app).toBeTruthy();
- }));
- it(`should have as title 'app'`, async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.debugElement.componentInstance;
- expect(app.title).toEqual('app');
- }));
- it('should render title in a h1 tag', async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
- }));
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ AppComponent
+ ],
+ }).compileComponents();
+ }));
+ it('should create the app', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app).toBeTruthy();
+ }));
+ it(`should have as title 'app'`, async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app.title).toEqual('app');
+ }));
+ it('should render title in a h1 tag', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
+ }));
});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 82c93e10f..3328004b0 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -20,41 +20,41 @@ import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { TranslationService, PageTitleService } from 'ng2-alfresco-core';
@Component({
- selector: 'app-root',
- templateUrl: './app.component.html',
- styleUrls: ['./app.component.scss']
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
- constructor(
- private route: ActivatedRoute,
- private router: Router,
- private pageTitle: PageTitleService,
- private translateService: TranslationService) {
- }
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private pageTitle: PageTitleService,
+ private translateService: TranslationService) {
+ }
- ngOnInit() {
- const { router, pageTitle, route, translateService } = this;
+ ngOnInit() {
+ const { router, pageTitle, route, translateService } = this;
- router
- .events
- .filter(event => event instanceof NavigationEnd)
- .subscribe(() => {
- let currentRoute = route.root;
+ router
+ .events
+ .filter(event => event instanceof NavigationEnd)
+ .subscribe(() => {
+ let currentRoute = route.root;
- while (currentRoute.firstChild) {
- currentRoute = currentRoute.firstChild;
- }
+ while (currentRoute.firstChild) {
+ currentRoute = currentRoute.firstChild;
+ }
- const snapshot: any = currentRoute.snapshot || {};
- const data: any = snapshot.data || {};
+ const snapshot: any = currentRoute.snapshot || {};
+ const data: any = snapshot.data || {};
- if (data.i18nTitle) {
- translateService.get(data.i18nTitle).subscribe(title => {
- pageTitle.setTitle(title);
- });
- } else {
- pageTitle.setTitle(data.title || '');
- }
- });
- }
+ if (data.i18nTitle) {
+ translateService.get(data.i18nTitle).subscribe(title => {
+ pageTitle.setTitle(title);
+ });
+ } else {
+ pageTitle.setTitle(data.title || '');
+ }
+ });
+ }
}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index da472df9c..1d3fb6ec9 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,26 +1,65 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
+import { TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
import { AdfModule } from './adf.module';
+import { CommonModule } from './common/common.module';
+import { MaterialModule } from './common/material.module';
+
import { AppComponent } from './app.component';
import { APP_ROUTES } from './app.routes';
import { LoginComponent } from './components/login/login.component';
+import { PreviewComponent } from './components/preview/preview.component';
+import { FilesComponent } from './components/files/files.component';
+import { FavoritesComponent } from './components/favorites/favorites.component';
+import { LibrariesComponent } from './components/libraries/libraries.component';
+import { RecentFilesComponent } from './components/recent-files/recent-files.component';
+import { SharedFilesComponent } from './components/shared-files/shared-files.component';
+import { TrashcanComponent } from './components/trashcan/trashcan.component';
+import { LayoutComponent } from './components/layout/layout.component';
+import { HeaderComponent } from './components/header/header.component';
+import { CurrentUserComponent } from './components/current-user/current-user.component';
+import { SearchComponent } from './components/search/search.component';
+import { SidenavComponent } from './components/sidenav/sidenav.component';
@NgModule({
- imports: [
- BrowserModule,
- RouterModule.forRoot(APP_ROUTES, {
- enableTracing: true
- }),
- AdfModule
- ],
- declarations: [
- AppComponent,
- LoginComponent
- ],
- providers: [],
- bootstrap: [AppComponent]
+ imports: [
+ BrowserModule,
+ RouterModule.forRoot(APP_ROUTES, {
+ enableTracing: true
+ }),
+ AdfModule,
+ CommonModule,
+ MaterialModule
+ ],
+ declarations: [
+ AppComponent,
+ LoginComponent,
+ LayoutComponent,
+ HeaderComponent,
+ CurrentUserComponent,
+ SearchComponent,
+ SidenavComponent,
+ FilesComponent,
+ FavoritesComponent,
+ LibrariesComponent,
+ RecentFilesComponent,
+ SharedFilesComponent,
+ TrashcanComponent,
+ PreviewComponent
+ ],
+ providers: [
+ {
+ provide: TRANSLATION_PROVIDER,
+ multi: true,
+ useValue: {
+ name: 'app',
+ source: 'assets'
+ }
+ }
+ ],
+ bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 7c65b7533..2cd07aba1 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -16,20 +16,111 @@
*/
import { Routes } from '@angular/router';
+import { AuthGuardEcm } from 'ng2-alfresco-core';
+
+import { LayoutComponent } from './components/layout/layout.component';
+
+import { FilesComponent } from './components/files/files.component';
+import { FavoritesComponent } from './components/favorites/favorites.component';
+import { LibrariesComponent } from './components/libraries/libraries.component';
+import { RecentFilesComponent } from './components/recent-files/recent-files.component';
+import { SharedFilesComponent } from './components/shared-files/shared-files.component';
+import { TrashcanComponent } from './components/trashcan/trashcan.component';
import { LoginComponent } from './components/login/login.component';
+import { PreviewComponent } from './components/preview/preview.component';
export const APP_ROUTES: Routes = [
- {
- path: '**',
- redirectTo: '/login',
- pathMatch: 'full'
- },
- {
- path: 'login',
- component: LoginComponent,
- data: {
- title: 'Sign in'
+ {
+ path: '',
+ component: LayoutComponent,
+ children: [
+ {
+ path: '',
+ redirectTo: `/personal-files`,
+ pathMatch: 'full'
+ },
+ {
+ path: 'favorites',
+ component: FavoritesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.FAVORITES.TITLE'
+ }
+ },
+ {
+ path: 'libraries',
+ children: [{
+ path: '',
+ component: LibrariesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.LIBRARIES.TITLE'
+ }
+ }, {
+ path: ':id',
+ component: FilesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.LIBRARIES.TITLE'
+ }
+ }]
+ },
+ {
+ path: 'personal-files',
+ children: [{
+ path: '',
+ component: FilesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.PERSONAL.TITLE',
+ defaultNodeId: '-my-'
+ }
+ }, {
+ path: ':id',
+ component: FilesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.PERSONAL.TITLE'
+ }
+ }]
+ },
+ {
+ path: 'recent-files',
+ component: RecentFilesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.RECENT.TITLE'
+ }
+ },
+ {
+ path: 'shared',
+ component: SharedFilesComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.SHARED.TITLE'
+ }
+ },
+ {
+ path: 'trashcan',
+ component: TrashcanComponent,
+ data: {
+ i18nTitle: 'APP.BROWSE.TRASHCAN.TITLE'
+ }
+ }
+ ],
+ canActivate: [
+ AuthGuardEcm
+ ]
+ },
+ {
+ path: 'preview/:nodeId',
+ component: PreviewComponent
+ },
+ {
+ path: '**',
+ redirectTo: '/login',
+ pathMatch: 'full'
+ },
+ {
+ path: 'login',
+ component: LoginComponent,
+ data: {
+ title: 'Sign in'
+ }
}
- }
];
+
diff --git a/src/app/common/adf.module.ts b/src/app/common/adf.module.ts
new file mode 100644
index 000000000..9698c33a5
--- /dev/null
+++ b/src/app/common/adf.module.ts
@@ -0,0 +1,44 @@
+/*!
+ * @license
+ * Copyright 2017 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 { NgModule } from '@angular/core';
+
+import { CoreModule } from 'ng2-alfresco-core';
+import { DataTableModule } from 'ng2-alfresco-datatable';
+import { DocumentListModule } from 'ng2-alfresco-documentlist';
+import { ViewerModule } from 'ng2-alfresco-viewer';
+import { UploadModule } from 'ng2-alfresco-upload';
+import { SearchModule } from 'ng2-alfresco-search';
+import { LoginModule } from 'ng2-alfresco-login';
+
+export function modules() {
+ return [
+ CoreModule,
+ DataTableModule,
+ DocumentListModule,
+ ViewerModule,
+ UploadModule,
+ SearchModule,
+ LoginModule
+ ];
+}
+
+@NgModule({
+ imports: modules(),
+ exports: modules()
+})
+export class AdfModule {}
diff --git a/src/app/common/common.module.ts b/src/app/common/common.module.ts
new file mode 100644
index 000000000..7dadb14f4
--- /dev/null
+++ b/src/app/common/common.module.ts
@@ -0,0 +1,93 @@
+/*!
+ * @license
+ * Copyright 2017 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 { NgModule } from '@angular/core';
+import { DatePipe } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { AdfModule } from './adf.module';
+import { MaterialModule } from './material.module';
+
+import { FolderDialogComponent } from './dialogs/folder-dialog.component';
+
+import { FolderCreateDirective } from './directives/folder-create.directive';
+import { FolderEditDirective } from './directives/folder-edit.directive';
+import { NodeCopyDirective } from './directives/node-copy.directive';
+import { NodeDeleteDirective } from './directives/node-delete.directive';
+import { NodeMoveDirective } from './directives/node-move.directive';
+import { DownloadFileDirective } from './directives/node-download.directive';
+import { NodeRestoreDirective } from './directives/node-restore.directive';
+import { NodePermanentDeleteDirective } from './directives/node-permanent-delete.directive';
+import { NodeFavoriteDirective } from './directives/node-favorite.directive';
+
+import { NodeNameTooltipPipe } from './pipes/node-name-tooltip.pipe';
+
+import { ContentManagementService } from './services/content-management.service';
+import { BrowsingFilesService } from './services/browsing-files.service';
+import { NodeActionsService } from './services/node-actions.service';
+
+export function modules() {
+ return [
+ FormsModule,
+ ReactiveFormsModule,
+ MaterialModule,
+ RouterModule,
+ AdfModule
+ ];
+}
+
+export function declarations() {
+ return [
+ FolderDialogComponent,
+
+ FolderCreateDirective,
+ FolderEditDirective,
+ NodeCopyDirective,
+ NodeDeleteDirective,
+ NodeMoveDirective,
+ DownloadFileDirective,
+ NodeRestoreDirective,
+ NodePermanentDeleteDirective,
+ NodeFavoriteDirective,
+
+ NodeNameTooltipPipe
+ ];
+}
+
+export function providers() {
+ return [
+ DatePipe,
+ BrowsingFilesService,
+ ContentManagementService,
+ NodeActionsService
+ ];
+}
+
+@NgModule({
+ imports: modules(),
+ declarations: declarations(),
+ entryComponents: [
+ FolderDialogComponent
+ ],
+ providers: providers(),
+ exports: [
+ ...modules(),
+ ...declarations()
+ ]
+})
+export class CommonModule {}
diff --git a/src/app/common/dialogs/folder-dialog.component.html b/src/app/common/dialogs/folder-dialog.component.html
new file mode 100644
index 000000000..c9777e6ae
--- /dev/null
+++ b/src/app/common/dialogs/folder-dialog.component.html
@@ -0,0 +1,62 @@
+
+ {{
+ (editing
+ ? 'APP.FOLDER_DIALOG.EDIT_FOLDER_TITLE'
+ : 'APP.FOLDER_DIALOG.CREATE_FOLDER_TITLE'
+ ) | translate
+ }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/common/dialogs/folder-dialog.component.spec.ts b/src/app/common/dialogs/folder-dialog.component.spec.ts
new file mode 100644
index 000000000..038ee6f80
--- /dev/null
+++ b/src/app/common/dialogs/folder-dialog.component.spec.ts
@@ -0,0 +1,261 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, async } from '@angular/core/testing';
+import { Observable } from 'rxjs/Rx';
+import { MdDialogModule, MdDialogRef } from '@angular/material';
+import { CoreModule, NodesApiService, TranslationService, NotificationService } from 'ng2-alfresco-core';
+
+import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
+import { FolderDialogComponent } from './folder-dialog.component';
+import { ComponentFixture } from '@angular/core/testing';
+
+describe('FolderDialogComponent', () => {
+
+ let dialogRefMock;
+ let fixture: ComponentFixture;
+ let component: FolderDialogComponent;
+ let translationService: TranslationService;
+ let nodesApi: NodesApiService;
+ let notificationService: NotificationService;
+ let dialogRef;
+
+ beforeEach(async(() => {
+ dialogRef = {
+ close: jasmine.createSpy('close')
+ };
+
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ MdDialogModule
+ ],
+ declarations: [
+ FolderDialogComponent
+ ],
+ providers: [
+ { provide: MdDialogRef, useValue: dialogRef }
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FolderDialogComponent);
+ component = fixture.componentInstance;
+
+ nodesApi = TestBed.get(NodesApiService);
+ notificationService = TestBed.get(NotificationService);
+
+ translationService = TestBed.get(TranslationService);
+ spyOn(translationService, 'get').and.returnValue(Observable.of('message'));
+ });
+
+ describe('Edit', () => {
+
+ beforeEach(() => {
+ component.data = {
+ folder: {
+ id: 'node-id',
+ name: 'folder-name',
+ properties: {
+ ['cm:description']: 'folder-description'
+ }
+ }
+ };
+ component.ngOnInit();
+ });
+
+ it('should init form with folder name and description', () => {
+ expect(component.name).toBe('folder-name');
+ expect(component.description).toBe('folder-description');
+ });
+
+ it('should update form input', () => {
+ component.form.controls['name'].setValue('folder-name-update');
+ component.form.controls['description'].setValue('folder-description-update');
+
+ expect(component.name).toBe('folder-name-update');
+ expect(component.description).toBe('folder-description-update');
+ });
+
+ it('should submit updated values if form is valid', () => {
+ spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of({}));
+
+ component.form.controls['name'].setValue('folder-name-update');
+ component.form.controls['description'].setValue('folder-description-update');
+
+ component.submit();
+
+ expect(nodesApi.updateNode).toHaveBeenCalledWith(
+ 'node-id',
+ {
+ name: 'folder-name-update',
+ properties: {
+ 'cm:title': 'folder-name-update',
+ 'cm:description': 'folder-description-update'
+ }
+ }
+ );
+ });
+
+ it('should call dialog to close with form data when submit is succesfluly', () => {
+ const folder = {
+ data: 'folder-data'
+ };
+
+ spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of(folder));
+
+ component.submit();
+
+ expect(dialogRef.close).toHaveBeenCalledWith(folder);
+ });
+
+ it('should not submit if form is invalid', () => {
+ spyOn(nodesApi, 'updateNode');
+
+ component.form.controls['name'].setValue('');
+ component.form.controls['description'].setValue('');
+
+ component.submit();
+
+ expect(component.form.valid).toBe(false);
+ expect(nodesApi.updateNode).not.toHaveBeenCalled();
+ });
+
+ it('should not call dialog to close if submit fails', () => {
+ spyOn(nodesApi, 'updateNode').and.returnValue(Observable.throw('error'));
+ spyOn(component, 'handleError').and.callFake(val => val);
+
+ component.submit();
+
+ expect(component.handleError).toHaveBeenCalled();
+ expect(dialogRef.close).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Create', () => {
+ beforeEach(() => {
+ component.data = {
+ parentNodeId: 'parentNodeId',
+ folder: null
+ };
+ component.ngOnInit();
+ });
+
+ it('should init form with empty inputs', () => {
+ expect(component.name).toBe('');
+ expect(component.description).toBe('');
+ });
+
+ it('should update form input', () => {
+ component.form.controls['name'].setValue('folder-name-update');
+ component.form.controls['description'].setValue('folder-description-update');
+
+ expect(component.name).toBe('folder-name-update');
+ expect(component.description).toBe('folder-description-update');
+ });
+
+ it('should submit updated values if form is valid', () => {
+ spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of({}));
+
+ component.form.controls['name'].setValue('folder-name-update');
+ component.form.controls['description'].setValue('folder-description-update');
+
+ component.submit();
+
+ expect(nodesApi.createFolder).toHaveBeenCalledWith(
+ 'parentNodeId',
+ {
+ name: 'folder-name-update',
+ properties: {
+ 'cm:title': 'folder-name-update',
+ 'cm:description': 'folder-description-update'
+ }
+ }
+ );
+ });
+
+ it('should call dialog to close with form data when submit is succesfluly', () => {
+ const folder = {
+ data: 'folder-data'
+ };
+
+ component.form.controls['name'].setValue('name');
+ component.form.controls['description'].setValue('description');
+
+ spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of(folder));
+
+ component.submit();
+
+ expect(dialogRef.close).toHaveBeenCalledWith(folder);
+ });
+
+ it('should not submit if form is invalid', () => {
+ spyOn(nodesApi, 'createFolder');
+
+ component.form.controls['name'].setValue('');
+ component.form.controls['description'].setValue('');
+
+ component.submit();
+
+ expect(component.form.valid).toBe(false);
+ expect(nodesApi.createFolder).not.toHaveBeenCalled();
+ });
+
+ it('should not call dialog to close if submit fails', () => {
+ spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw('error'));
+ spyOn(component, 'handleError').and.callFake(val => val);
+
+ component.form.controls['name'].setValue('name');
+ component.form.controls['description'].setValue('description');
+
+ component.submit();
+
+ expect(component.handleError).toHaveBeenCalled();
+ expect(dialogRef.close).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('handleError()', () => {
+ it('should raise error for 409', () => {
+ spyOn(notificationService, 'openSnackMessage').and.stub();
+
+ const error = {
+ message: '{ "error": { "statusCode" : 409 } }'
+ };
+
+ component.handleError(error);
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translationService.get).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.EXISTENT_FOLDER');
+ });
+
+ it('should raise generic error', () => {
+ spyOn(notificationService, 'openSnackMessage').and.stub();
+
+ const error = {
+ message: '{ "error": { "statusCode" : 123 } }'
+ };
+
+ component.handleError(error);
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translationService.get).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC');
+ });
+ });
+});
diff --git a/src/app/common/dialogs/folder-dialog.component.ts b/src/app/common/dialogs/folder-dialog.component.ts
new file mode 100644
index 000000000..36423498a
--- /dev/null
+++ b/src/app/common/dialogs/folder-dialog.component.ts
@@ -0,0 +1,138 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Observable } from 'rxjs/Rx';
+
+import { Component, Inject, Optional } from '@angular/core';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
+
+import { TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+import { forbidSpecialCharacters, forbidEndingDot, forbidOnlySpaces } from './folder-name.validators';
+
+@Component({
+ selector: 'folder-dialog',
+ templateUrl: './folder-dialog.component.html'
+})
+export class FolderDialogComponent {
+ form: FormGroup;
+ folder: MinimalNodeEntryEntity = null;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private dialog: MdDialogRef,
+ private nodesApi: NodesApiService,
+ private translation: TranslationService,
+ private notification: NotificationService,
+ @Optional()
+ @Inject(MD_DIALOG_DATA)
+ public data: any
+ ) {}
+
+ get editing(): boolean {
+ return !!this.data.folder;
+ }
+
+ ngOnInit() {
+ const { folder } = this.data;
+ let name = '', description = '';
+
+ if (folder) {
+ const { properties } = folder;
+
+ name = folder.name || '';
+ description = properties ? properties['cm:description'] : '';
+ }
+
+ const validators = {
+ name: [
+ Validators.required,
+ forbidSpecialCharacters,
+ forbidEndingDot,
+ forbidOnlySpaces
+ ]
+ };
+
+ this.form = this.formBuilder.group({
+ name: [ name, validators.name ],
+ description: [ description ]
+ });
+ }
+
+ get name(): string {
+ let { name } = this.form.value;
+
+ return (name || '').trim();
+ }
+
+ get description(): string {
+ let { description } = this.form.value;
+
+ return (description || '').trim();
+ }
+
+ private get properties(): any {
+ const { name: title, description } = this;
+
+ return {
+ 'cm:title': title,
+ 'cm:description': description
+ };
+ }
+
+ private create(): Observable {
+ const { name, properties, nodesApi, data: { parentNodeId} } = this;
+ return nodesApi.createFolder(parentNodeId, { name, properties });
+ }
+
+ private edit(): Observable {
+ const { name, properties, nodesApi, data: { folder: { id: nodeId }} } = this;
+ return nodesApi.updateNode(nodeId, { name, properties });
+ }
+
+ submit() {
+ const { form, dialog, editing } = this;
+
+ if (!form.valid) { return; }
+
+ (editing ? this.edit() : this.create())
+ .subscribe(
+ (folder: MinimalNodeEntryEntity) => dialog.close(folder),
+ (error) => this.handleError(error)
+ );
+ }
+
+ handleError(error: any): any {
+ let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
+
+ try {
+ const { error: { statusCode } } = JSON.parse(error.message);
+
+ if (statusCode === 409) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.EXISTENT_FOLDER';
+ }
+ } catch (err) { /* Do nothing, keep the original message */ }
+
+ this.translation.get(i18nMessageString).subscribe(message => {
+ this.notification.openSnackMessage(message, 3000);
+ });
+
+ return error;
+ }
+}
diff --git a/src/app/common/dialogs/folder-name.validators.ts b/src/app/common/dialogs/folder-name.validators.ts
new file mode 100644
index 000000000..714ac127f
--- /dev/null
+++ b/src/app/common/dialogs/folder-name.validators.ts
@@ -0,0 +1,45 @@
+/*!
+ * @license
+ * Copyright 2017 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 { FormControl } from '@angular/forms';
+
+const I18N_ERRORS_PATH = 'APP.FOLDER_DIALOG.FOLDER_NAME.ERRORS';
+
+export function forbidSpecialCharacters({ value }: FormControl) {
+ const specialCharacters: RegExp = /([\*\"\<\>\\\/\?\:\|])/;
+ const isValid: boolean = !specialCharacters.test(value);
+
+ return (isValid) ? null : {
+ message: `${I18N_ERRORS_PATH}.SPECIAL_CHARACTERS`
+ };
+}
+
+export function forbidEndingDot({ value }: FormControl) {
+ const isValid: boolean = ((value || '').split('').pop() !== '.');
+
+ return isValid ? null : {
+ message: `${I18N_ERRORS_PATH}.ENDING_DOT`
+ };
+}
+
+export function forbidOnlySpaces({ value }: FormControl) {
+ const isValid: boolean = !!((value || '')).trim();
+
+ return isValid ? null : {
+ message: `${I18N_ERRORS_PATH}.ONLY_SPACES`
+ };
+}
diff --git a/src/app/common/directives/folder-create.directive.spec.ts b/src/app/common/directives/folder-create.directive.spec.ts
new file mode 100644
index 000000000..689b75e37
--- /dev/null
+++ b/src/app/common/directives/folder-create.directive.spec.ts
@@ -0,0 +1,96 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, ComponentFixture } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Component } from '@angular/core';
+import { Observable } from 'rxjs/Rx';
+import { MdDialogModule, MdDialog } from '@angular/material';
+
+import { FolderCreateDirective } from './folder-create.directive';
+import { ContentManagementService } from '../services/content-management.service';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ parentNode = '';
+}
+
+describe('FolderCreateDirective', () => {
+ let fixture: ComponentFixture;
+ let element;
+ let node: any;
+ let dialog: MdDialog;
+ let contentService: ContentManagementService;
+ let dialogRefMock;
+
+ const event: any = {
+ type: 'click',
+ preventDefault: () => null
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ MdDialogModule ],
+ declarations: [
+ TestComponent,
+ FolderCreateDirective
+ ]
+ ,
+ providers: [
+ ContentManagementService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ element = fixture.debugElement.query(By.directive(FolderCreateDirective));
+ dialog = TestBed.get(MdDialog);
+ contentService = TestBed.get(ContentManagementService);
+ });
+
+ beforeEach(() => {
+ node = { entry: { id: 'nodeId' } };
+
+ dialogRefMock = {
+ afterClosed: val => Observable.of(val)
+ };
+
+ spyOn(dialog, 'open').and.returnValue(dialogRefMock);
+ });
+
+ it('emits createFolder event when input value is not undefined', () => {
+ spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(node));
+
+ contentService.createFolder.subscribe((val) => {
+ expect(val).toBe(node);
+ });
+
+ element.triggerEventHandler('click', event);
+ fixture.detectChanges();
+ });
+
+ it('does not emits createFolder event when input value is undefined', () => {
+ spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(null));
+ spyOn(contentService.createFolder, 'next');
+
+ element.triggerEventHandler('click', event);
+ fixture.detectChanges();
+
+ expect(contentService.createFolder.next).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/app/common/directives/folder-create.directive.ts b/src/app/common/directives/folder-create.directive.ts
new file mode 100644
index 000000000..492124b55
--- /dev/null
+++ b/src/app/common/directives/folder-create.directive.ts
@@ -0,0 +1,66 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+import { MdDialog, MdDialogConfig } from '@angular/material';
+
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+import { FolderDialogComponent } from '../dialogs/folder-dialog.component';
+import { ContentManagementService } from '../services/content-management.service';
+
+@Directive({
+ selector: '[app-create-folder]'
+})
+export class FolderCreateDirective {
+ static DIALOG_WIDTH: number = 400;
+
+ @Input('app-create-folder')
+ parentNodeId: string;
+
+ @HostListener('click', [ '$event' ])
+ onClick(event) {
+ event.preventDefault();
+ this.openDialog();
+ }
+
+ constructor(
+ public dialogRef: MdDialog,
+ public content: ContentManagementService
+ ) {}
+
+ private get dialogConfig(): MdDialogConfig {
+ const { DIALOG_WIDTH: width } = FolderCreateDirective;
+ const { parentNodeId } = this;
+
+ return {
+ data: { parentNodeId },
+ width: `${width}px`
+ };
+ }
+
+ private openDialog(): void {
+ const { dialogRef, dialogConfig, content } = this;
+ const dialogInstance = dialogRef.open(FolderDialogComponent, dialogConfig);
+
+ dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
+ if (node) {
+ content.createFolder.next(node);
+ }
+ });
+ }
+}
diff --git a/src/app/common/directives/folder-edit.directive.spec.ts b/src/app/common/directives/folder-edit.directive.spec.ts
new file mode 100644
index 000000000..2b9013f4c
--- /dev/null
+++ b/src/app/common/directives/folder-edit.directive.spec.ts
@@ -0,0 +1,96 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, ComponentFixture } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Component } from '@angular/core';
+import { Observable } from 'rxjs/Rx';
+import { MdDialogModule, MdDialog } from '@angular/material';
+
+import { FolderEditDirective } from './folder-edit.directive';
+import { ContentManagementService } from '../services/content-management.service';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ folder = {};
+}
+
+describe('FolderEditDirective', () => {
+ let fixture: ComponentFixture;
+ let element;
+ let node: any;
+ let dialog: MdDialog;
+ let contentService: ContentManagementService;
+ let dialogRefMock;
+
+ const event = {
+ type: 'click',
+ preventDefault: () => null
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ MdDialogModule ],
+ declarations: [
+ TestComponent,
+ FolderEditDirective
+ ]
+ ,
+ providers: [
+ ContentManagementService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ element = fixture.debugElement.query(By.directive(FolderEditDirective));
+ dialog = TestBed.get(MdDialog);
+ contentService = TestBed.get(ContentManagementService);
+ });
+
+ beforeEach(() => {
+ node = { entry: { id: 'folderId' } };
+
+ dialogRefMock = {
+ afterClosed: val => Observable.of(val)
+ };
+
+ spyOn(dialog, 'open').and.returnValue(dialogRefMock);
+ });
+
+ it('emits editFolder event when input value is not undefined', () => {
+ spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(node));
+
+ contentService.createFolder.subscribe((val) => {
+ expect(val).toBe(node);
+ });
+
+ element.triggerEventHandler('click', event);
+ fixture.detectChanges();
+ });
+
+ it('does not emits FolderEditDirective event when input value is undefined', () => {
+ spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(null));
+ spyOn(contentService.createFolder, 'next');
+
+ element.triggerEventHandler('click', event);
+ fixture.detectChanges();
+
+ expect(contentService.createFolder.next).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/app/common/directives/folder-edit.directive.ts b/src/app/common/directives/folder-edit.directive.ts
new file mode 100644
index 000000000..89cc06f03
--- /dev/null
+++ b/src/app/common/directives/folder-edit.directive.ts
@@ -0,0 +1,67 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, ElementRef, Input } from '@angular/core';
+import { MdDialog, MdDialogConfig } from '@angular/material';
+
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+import { FolderDialogComponent } from '../dialogs/folder-dialog.component';
+import { ContentManagementService } from '../services/content-management.service';
+
+@Directive({
+ selector: '[app-edit-folder]'
+})
+export class FolderEditDirective {
+ static DIALOG_WIDTH: number = 400;
+
+ @Input('app-edit-folder')
+ folder: MinimalNodeEntryEntity;
+
+ @HostListener('click', [ '$event' ])
+ onClick(event) {
+ event.preventDefault();
+ this.openDialog();
+ }
+
+ constructor(
+ public dialogRef: MdDialog,
+ public elementRef: ElementRef,
+ public content: ContentManagementService
+ ) {}
+
+ private get dialogConfig(): MdDialogConfig {
+ const { DIALOG_WIDTH: width } = FolderEditDirective;
+ const { folder } = this;
+
+ return {
+ data: { folder },
+ width: `${width}px`
+ };
+ }
+
+ private openDialog(): void {
+ const { dialogRef, dialogConfig, content } = this;
+ const dialogInstance = dialogRef.open(FolderDialogComponent, dialogConfig);
+
+ dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
+ if (node) {
+ content.editFolder.next(node);
+ }
+ });
+ }
+}
diff --git a/src/app/common/directives/node-copy.directive.spec.ts b/src/app/common/directives/node-copy.directive.spec.ts
new file mode 100644
index 000000000..f17390936
--- /dev/null
+++ b/src/app/common/directives/node-copy.directive.spec.ts
@@ -0,0 +1,249 @@
+import { Component, DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { Observable } from 'rxjs/Rx';
+
+import { CoreModule, TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { DocumentListModule } from 'ng2-alfresco-documentlist';
+
+import { NodeActionsService } from '../services/node-actions.service';
+import { ContentManagementService } from '../services/content-management.service';
+import { NodeCopyDirective } from './node-copy.directive';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ selection;
+}
+
+describe('NodeCopyDirective', () => {
+ let fixture: ComponentFixture;
+ let component: TestComponent;
+ let element: DebugElement;
+ let notificationService: NotificationService;
+ let nodesApiService: NodesApiService;
+ let service: NodeActionsService;
+ let translationService: TranslationService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ DocumentListModule
+ ],
+ declarations: [
+ TestComponent,
+ NodeCopyDirective
+ ],
+ providers: [
+ ContentManagementService,
+ NodeActionsService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodeCopyDirective));
+ notificationService = TestBed.get(NotificationService);
+ nodesApiService = TestBed.get(NodesApiService);
+ service = TestBed.get(NodeActionsService);
+ translationService = TestBed.get(TranslationService);
+ });
+
+ beforeEach(() => {
+ spyOn(translationService, 'get').and.callFake((key) => {
+ return Observable.of(key);
+ });
+ });
+
+ describe('Copy node action', () => {
+ beforeEach(() => {
+ spyOn(notificationService, 'openSnackMessageAction').and.callThrough();
+ });
+
+ it('notifies successful copy of a node', () => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+ const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_COPY.SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies successful copy of multiple nodes', () => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
+
+ component.selection = [
+ { entry: { id: 'node-to-copy-1', name: 'name1' } },
+ { entry: { id: 'node-to-copy-2', name: 'name2' } }];
+ const createdItems = [
+ { entry: { id: 'copy-of-node-1', name: 'name1' } },
+ { entry: { id: 'copy-of-node-2', name: 'name2' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_COPY.PLURAL', 'Undo', 10000
+ );
+ });
+
+ it('notifies error if success message was not emitted', () => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.of(''));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next();
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC', '', 3000);
+ });
+
+ it('notifies permission error on copy of node', () => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
+
+ component.selection = [{ entry: { id: '1', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.PERMISSION', '', 3000
+ );
+ });
+
+ it('notifies generic error message on all errors, but 403', () => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
+
+ component.selection = [{ entry: { id: '1', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.GENERIC', '', 3000
+ );
+ });
+ });
+
+ describe('Undo Copy action', () => {
+ beforeEach(() => {
+ spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
+
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({
+ onAction: () => Observable.of({})
+ });
+ });
+
+ it('should delete the newly created node on Undo action', () => {
+ spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of(null));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+ const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_COPY.SINGULAR', 'Undo', 10000
+ );
+
+ expect(nodesApiService.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true });
+ });
+
+ it('should delete also the node created inside an already existing folder from destination', () => {
+ const spyOnDeleteNode = spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of(null));
+
+ component.selection = [
+ { entry: { id: 'node-to-copy-1', name: 'name1' } },
+ { entry: { id: 'node-to-copy-2', name: 'folder-with-name-already-existing-on-destination' } }];
+ const id1 = 'copy-of-node-1';
+ const id2 = 'copy-of-child-of-node-2';
+ const createdItems = [
+ { entry: { id: id1, name: 'name1' } },
+ [ { entry: { id: id2, name: 'name-of-child-of-node-2' , parentId: 'the-folder-already-on-destination' } }] ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_COPY.PLURAL', 'Undo', 10000
+ );
+
+ expect(spyOnDeleteNode).toHaveBeenCalled();
+ expect(spyOnDeleteNode.calls.allArgs())
+ .toEqual([[id1, { permanent: true }], [id2, { permanent: true }]]);
+ });
+
+ it('notifies when error occurs on Undo action', () => {
+ spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(null));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+ const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(nodesApiService.deleteNode).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction['calls'].allArgs())
+ .toEqual([['APP.MESSAGES.INFO.NODE_COPY.SINGULAR', 'Undo', 10000],
+ ['APP.MESSAGES.ERRORS.GENERIC', '', 3000]]);
+ });
+
+ it('notifies when some error of type Error occurs on Undo action', () => {
+ spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(new Error('oops!')));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+ const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(nodesApiService.deleteNode).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction['calls'].allArgs())
+ .toEqual([['APP.MESSAGES.INFO.NODE_COPY.SINGULAR', 'Undo', 10000],
+ ['APP.MESSAGES.ERRORS.GENERIC', '', 3000]]);
+ });
+
+ it('notifies permission error when it occurs on Undo action', () => {
+ spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
+
+ component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
+ const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentCopied.next(createdItems);
+
+ expect(service.copyNodes).toHaveBeenCalled();
+ expect(nodesApiService.deleteNode).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction['calls'].allArgs())
+ .toEqual([['APP.MESSAGES.INFO.NODE_COPY.SINGULAR', 'Undo', 10000],
+ ['APP.MESSAGES.ERRORS.PERMISSION', '', 3000]]);
+ });
+ });
+
+});
diff --git a/src/app/common/directives/node-copy.directive.ts b/src/app/common/directives/node-copy.directive.ts
new file mode 100644
index 000000000..23c1b41e3
--- /dev/null
+++ b/src/app/common/directives/node-copy.directive.ts
@@ -0,0 +1,125 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+import { Observable } from 'rxjs/Rx';
+
+import { TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+import { NodeActionsService } from '../services/node-actions.service';
+import { ContentManagementService } from '../services/content-management.service';
+
+@Directive({
+ selector: '[app-copy-node]'
+})
+export class NodeCopyDirective {
+
+ @Input('app-copy-node')
+ selection: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.copySelected();
+ }
+
+ constructor(
+ private content: ContentManagementService,
+ private notification: NotificationService,
+ private nodeActionsService: NodeActionsService,
+ private nodesApi: NodesApiService,
+ private translation: TranslationService
+ ) {}
+
+ copySelected() {
+ Observable.zip(
+ this.nodeActionsService.copyNodes(this.selection),
+ this.nodeActionsService.contentCopied
+ ).subscribe(
+ (result) => {
+ const [ operationResult, newItems ] = result;
+ this.toastMessage(operationResult, newItems);
+ },
+ (error) => {
+ this.toastMessage(error);
+ }
+ );
+ }
+
+ private toastMessage(info: any, newItems?: MinimalNodeEntity[]) {
+ const numberOfCopiedItems = newItems ? newItems.length : '';
+
+ let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
+
+ if (typeof info === 'string') {
+ if (info.toLowerCase().indexOf('succes') !== -1) {
+
+ const i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'SINGULAR' : 'PLURAL';
+ i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
+ }
+
+ } else {
+ try {
+
+ const { error: { statusCode } } = JSON.parse(info.message);
+
+ if (statusCode === 403) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
+ }
+
+ } catch (err) { /* Do nothing, keep the original message */ }
+ }
+
+ const undo = (numberOfCopiedItems > 0) ? 'Undo' : '';
+ const withUndo = (numberOfCopiedItems > 0) ? '_WITH_UNDO' : '';
+
+ this.translation.get(i18nMessageString, { number: numberOfCopiedItems }).subscribe(message => {
+ this.notification.openSnackMessageAction(message, undo, NodeActionsService[`SNACK_MESSAGE_DURATION${withUndo}`])
+ .onAction()
+ .subscribe(() => this.deleteCopy(newItems));
+ });
+ }
+
+ private deleteCopy(nodes: MinimalNodeEntity[]) {
+ const batch = this.nodeActionsService.flatten(nodes)
+ .filter(item => item.entry)
+ .map(item => this.nodesApi.deleteNode(item.entry.id, { permanent: true }));
+
+ Observable.forkJoin(...batch)
+ .subscribe(
+ () => {
+ this.content.deleteNode.next(null);
+ },
+ (error) => {
+ let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
+
+ let errorJson = null;
+ try {
+ errorJson = JSON.parse(error.message);
+ } catch (e) { //
+ }
+
+ if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
+ }
+
+ this.translation.get(i18nMessageString).subscribe(message => {
+ this.notification.openSnackMessageAction(message, '', NodeActionsService.SNACK_MESSAGE_DURATION);
+ });
+ }
+ );
+ }
+}
diff --git a/src/app/common/directives/node-delete.directive.spec.ts b/src/app/common/directives/node-delete.directive.spec.ts
new file mode 100644
index 000000000..cf91a10ac
--- /dev/null
+++ b/src/app/common/directives/node-delete.directive.spec.ts
@@ -0,0 +1,246 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, ComponentFixture, fakeAsync } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { CoreModule, TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { Component, DebugElement } from '@angular/core';
+import { NodeDeleteDirective } from './node-delete.directive';
+import { ContentManagementService } from '../services/content-management.service';
+import { Observable } from 'rxjs/Rx';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ selection;
+}
+
+describe('NodeDeleteDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ let element: DebugElement;
+ let notificationService: NotificationService;
+ let translationService: TranslationService;
+ let contentService: ContentManagementService;
+ let nodeApiService: NodesApiService;
+ let spySnackBar;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule
+ ],
+ declarations: [
+ TestComponent,
+ NodeDeleteDirective
+ ],
+ providers: [
+ ContentManagementService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodeDeleteDirective));
+ notificationService = TestBed.get(NotificationService);
+ translationService = TestBed.get(TranslationService);
+ nodeApiService = TestBed.get(NodesApiService);
+ contentService = TestBed.get(ContentManagementService);
+ });
+
+ beforeEach(() => {
+ spyOn(translationService, 'get').and.callFake((key) => {
+ return Observable.of(key);
+ });
+ });
+
+ describe('Delete action', () => {
+ beforeEach(() => {
+ spyOn(notificationService, 'openSnackMessageAction').and.callThrough();
+ });
+
+ it('notifies file deletion', () => {
+ spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null));
+
+ component.selection = [{ entry: { id: '1', name: 'name1' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies faild file deletion', () => {
+ spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.throw(null));
+
+ component.selection = [{ entry: { id: '1', name: 'name1' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION', '', 10000
+ );
+ });
+
+ it('notifies files deletion', () => {
+ spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL', 'Undo', 10000
+ );
+ });
+
+ it('notifies faild files deletion', () => {
+ spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.throw(null));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', '', 10000
+ );
+ });
+
+ it('notifies partial deletion when only one file is successful', () => {
+ spyOn(nodeApiService, 'deleteNode').and.callFake((id) => {
+ if (id === '1') {
+ return Observable.throw(null);
+ } else {
+ return Observable.of(null);
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies partial deletion when some files are successful', () => {
+ spyOn(nodeApiService, 'deleteNode').and.callFake((id) => {
+ if (id === '1') {
+ return Observable.throw(null);
+ }
+
+ if (id === '2') {
+ return Observable.of(null);
+ }
+
+ if (id === '3') {
+ return Observable.of(null);
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } },
+ { entry: { id: '3', name: 'name3' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', 'Undo', 10000
+ );
+ });
+ });
+
+ describe('Restore action', () => {
+ beforeEach(() => {
+ spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null));
+
+ spySnackBar = spyOn(notificationService, 'openSnackMessageAction').and.returnValue({
+ onAction: () => Observable.of({})
+ });
+ });
+
+ it('notifies faild file on on restore', () => {
+ spyOn(nodeApiService, 'restoreNode').and.returnValue(Observable.throw(null));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(spySnackBar.calls.mostRecent().args)
+ .toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE', '', 3000]));
+ });
+
+ it('notifies faild files on on restore', () => {
+ spyOn(nodeApiService, 'restoreNode').and.returnValue(Observable.throw(null));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(spySnackBar.calls.mostRecent().args)
+ .toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', '', 3000]));
+ });
+
+ it('signals files restored', () => {
+ spyOn(contentService.restoreNode, 'next');
+ spyOn(nodeApiService, 'restoreNode').and.callFake((id) => {
+ if (id === '1') {
+ return Observable.of(null);
+ } else {
+ return Observable.throw(null);
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(contentService.restoreNode.next).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/common/directives/node-delete.directive.ts b/src/app/common/directives/node-delete.directive.ts
new file mode 100644
index 000000000..95502af98
--- /dev/null
+++ b/src/app/common/directives/node-delete.directive.ts
@@ -0,0 +1,237 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+
+import { TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+import { Observable } from 'rxjs/Rx';
+
+import { ContentManagementService } from '../services/content-management.service';
+
+@Directive({
+ selector: '[app-delete-node]'
+})
+export class NodeDeleteDirective {
+ static RESTORE_MESSAGE_DURATION: number = 3000;
+ static DELETE_MESSAGE_DURATION: number = 10000;
+
+ @Input('app-delete-node')
+ selection: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.deleteSelected();
+ }
+
+ constructor(
+ private nodesApi: NodesApiService,
+ private notification: NotificationService,
+ private content: ContentManagementService,
+ private translation: TranslationService
+ ) {}
+
+ private deleteSelected(): void {
+ const batch = [];
+
+ this.selection.forEach((node) => {
+ batch.push(this.performAction('delete', node.entry));
+ });
+
+ Observable.forkJoin(...batch)
+ .subscribe(
+ (data) => {
+ const processedData = this.processStatus(data);
+
+ this.getDeleteMesssage(processedData)
+ .subscribe((message) => {
+ const withUndo = processedData.someSucceeded ? 'Undo' : '';
+
+ this.notification.openSnackMessageAction(message, withUndo, NodeDeleteDirective.DELETE_MESSAGE_DURATION)
+ .onAction()
+ .subscribe(() => this.restore(processedData.success));
+
+ if (processedData.someSucceeded) {
+ this.content.deleteNode.next(null);
+ }
+ });
+ }
+ );
+ }
+
+ private restore(items): void {
+ const batch = [];
+
+ items.forEach((item) => {
+ batch.push(this.performAction('restore', item));
+ });
+
+ Observable.forkJoin(...batch)
+ .subscribe(
+ (data) => {
+ const processedData = this.processStatus(data);
+
+ if (processedData.failed.length) {
+ this.getRestoreMessage(processedData)
+ .subscribe((message) => {
+ this.notification.openSnackMessageAction(
+ message, '' , NodeDeleteDirective.RESTORE_MESSAGE_DURATION
+ );
+ });
+ }
+
+ if (processedData.someSucceeded) {
+ this.content.restoreNode.next(null);
+ }
+ }
+ );
+ }
+
+ private performAction(action: string, item: any): Observable {
+ const { name } = item;
+ // Check if there's nodeId for Shared Files
+ const id = item.nodeId || item.id;
+
+ let performedAction: any = null;
+
+ if (action === 'delete') {
+ performedAction = this.nodesApi.deleteNode(id);
+ } else {
+ performedAction = this.nodesApi.restoreNode(id);
+ }
+
+ return performedAction
+ .map(() => {
+ return {
+ id,
+ name,
+ status: 1
+ };
+ })
+ .catch((error: any) => {
+ return Observable.of({
+ id,
+ name,
+ status: 0
+ });
+ });
+ }
+
+ private processStatus(data): any {
+ const deleteStatus = {
+ success: [],
+ failed: [],
+ get someFailed() {
+ return !!(this.failed.length);
+ },
+ get someSucceeded() {
+ return !!(this.success.length);
+ },
+ get oneFailed() {
+ return this.failed.length === 1;
+ },
+ get oneSucceeded() {
+ return this.success.length === 1;
+ },
+ get allSucceeded() {
+ return this.someSucceeded && !this.someFailed;
+ },
+ get allFailed() {
+ return this.someFailed && !this.someSucceeded;
+ }
+ };
+
+ return data.reduce(
+ (acc, next) => {
+ if (next.status === 1) {
+ acc.success.push(next);
+ } else {
+ acc.failed.push(next);
+ }
+
+ return acc;
+ },
+ deleteStatus
+ );
+ }
+
+ private getRestoreMessage(status): Observable {
+ if (status.someFailed && !status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL',
+ { number: status.failed.length }
+ );
+ }
+
+ if (status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.NODE_RESTORE',
+ { name: status.failed[0].name }
+ );
+ }
+ }
+
+ private getDeleteMesssage(status): Observable {
+ if (status.allFailed && !status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL',
+ { number: status.failed.length }
+ );
+ }
+
+ if (status.allSucceeded && !status.oneSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL',
+ { number: status.success.length }
+ );
+ }
+
+ if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL',
+ {
+ success: status.success.length,
+ failed: status.failed.length
+ }
+ );
+ }
+
+ if (status.someFailed && status.oneSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR',
+ {
+ success: status.success.length,
+ failed: status.failed.length
+ }
+ );
+ }
+
+ if (status.oneFailed && !status.someSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION',
+ { name: status.failed[0].name }
+ );
+ }
+
+ if (status.oneSucceeded && !status.someFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR',
+ { name: status.success[0].name }
+ );
+ }
+ }
+}
diff --git a/src/app/common/directives/node-download.directive.spec.ts b/src/app/common/directives/node-download.directive.spec.ts
new file mode 100644
index 000000000..a239c2cb7
--- /dev/null
+++ b/src/app/common/directives/node-download.directive.spec.ts
@@ -0,0 +1,142 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, ComponentFixture } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { CoreModule, AlfrescoApiService } from 'ng2-alfresco-core';
+import { MdDialog } from '@angular/material';
+import { Component, DebugElement } from '@angular/core';
+import { DownloadFileDirective } from './node-download.directive';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ selection;
+}
+
+describe('DownloadFileDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ let element: DebugElement;
+ let dialog: MdDialog;
+ let apiService: AlfrescoApiService;
+ let contentService;
+ let spySnackBar;
+ let dialogSpy;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule
+ ],
+ declarations: [
+ TestComponent,
+ DownloadFileDirective
+ ],
+ providers: [
+ AlfrescoApiService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(DownloadFileDirective));
+ dialog = TestBed.get(MdDialog);
+ apiService = TestBed.get(AlfrescoApiService);
+ contentService = apiService.getInstance().content;
+ dialogSpy = spyOn(dialog, 'open');
+ });
+
+ it('should not download node when selection is empty', () => {
+ spyOn(apiService, 'getInstance');
+ component.selection = [];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(apiService.getInstance).not.toHaveBeenCalled();
+ });
+
+ it('should not download zip when selection has no nodes', () => {
+ component.selection = [];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(dialogSpy).not.toHaveBeenCalled();
+ });
+
+ it('should download selected node as file', () => {
+ spyOn(contentService, 'getContentUrl');
+ const node = { entry: { id: 'node-id', isFile: true } };
+ component.selection = [node];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(contentService.getContentUrl).toHaveBeenCalledWith(node.entry.id, true);
+ });
+
+ it('should download selected files nodes as zip', () => {
+ const node1 = { entry: { id: 'node-1' } };
+ const node2 = { entry: { id: 'node-2' } };
+ component.selection = [node1, node2];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(dialogSpy.calls.argsFor(0)[1].data).toEqual({ nodeIds: [ 'node-1', 'node-2' ] });
+ });
+
+ it('should download selected folder node as zip', () => {
+ const node = { entry: { isFolder: true, id: 'node-id' } };
+ component.selection = [node];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(dialogSpy.calls.argsFor(0)[1].data).toEqual({ nodeIds: [ 'node-id' ] });
+ });
+
+ it('should create link element to download file node', () => {
+ const dummyLinkElement = {
+ download: null,
+ href: null,
+ click: () => null,
+ style: {
+ display: null
+ }
+ };
+
+ const node = { entry: { name: 'dummy', isFile: true, id: 'node-id' } };
+
+ spyOn(contentService, 'getContentUrl').and.returnValue('somewhere-over-the-rainbow');
+ spyOn(document, 'createElement').and.returnValue(dummyLinkElement);
+ spyOn(document.body, 'appendChild').and.stub();
+ spyOn(document.body, 'removeChild').and.stub();
+
+ component.selection = [node];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(document.createElement).toHaveBeenCalled();
+ expect(dummyLinkElement.download).toBe('dummy');
+ expect(dummyLinkElement.href).toContain('somewhere-over-the-rainbow');
+ });
+});
diff --git a/src/app/common/directives/node-download.directive.ts b/src/app/common/directives/node-download.directive.ts
new file mode 100644
index 000000000..83c55a80e
--- /dev/null
+++ b/src/app/common/directives/node-download.directive.ts
@@ -0,0 +1,110 @@
+/*!
+ * @license
+ * Copyright 2017 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, Input, HostListener } from '@angular/core';
+import { MdDialog } from '@angular/material';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+import { AlfrescoApiService, DownloadZipDialogComponent } from 'ng2-alfresco-core';
+
+@Directive({
+ selector: '[app-download-node]'
+})
+export class DownloadFileDirective {
+ @Input('app-download-node')
+ nodes: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.downloadNodes(this.nodes);
+ }
+
+ constructor(
+ private apiService: AlfrescoApiService,
+ private dialog: MdDialog
+ ) {}
+
+ private downloadNodes(selection: Array) {
+ if (!selection || selection.length === 0) {
+ return;
+ }
+
+ if (selection.length === 1) {
+ this.downloadNode(selection[0]);
+ } else {
+ this.downloadZip(selection);
+ }
+ }
+
+ private downloadNode(node: MinimalNodeEntity) {
+ if (node && node.entry) {
+ const entry = node.entry;
+
+ if (entry.isFile) {
+ this.downloadFile(node);
+ }
+
+ if (entry.isFolder) {
+ this.downloadZip([node]);
+ }
+
+ // Check if there's nodeId for Shared Files
+ if (!entry.isFile && !entry.isFolder && (entry).nodeId) {
+ this.downloadFile(node);
+ }
+ }
+ }
+
+ private downloadFile(node: MinimalNodeEntity) {
+ if (node && node.entry) {
+ const contentApi = this.apiService.getInstance().content;
+
+ const url = contentApi.getContentUrl(node.entry.id, true);
+ const fileName = node.entry.name;
+
+ this.download(url, fileName);
+ }
+ }
+
+ private downloadZip(selection: Array) {
+ if (selection && selection.length > 0) {
+ // nodeId for Shared node
+ const nodeIds = selection.map((node: any) => (node.entry.nodeId || node.entry.id));
+
+ this.dialog.open(DownloadZipDialogComponent, {
+ width: '600px',
+ disableClose: true,
+ data: {
+ nodeIds
+ }
+ });
+ }
+ }
+
+ private download(url: string, fileName: string) {
+ if (url && fileName) {
+ const link = document.createElement('a');
+
+ link.style.display = 'none';
+ link.download = fileName;
+ link.href = url;
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+ }
+}
diff --git a/src/app/common/directives/node-favorite.directive.spec.ts b/src/app/common/directives/node-favorite.directive.spec.ts
new file mode 100644
index 000000000..325e3e2ea
--- /dev/null
+++ b/src/app/common/directives/node-favorite.directive.spec.ts
@@ -0,0 +1,332 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { CoreModule, TranslationService, NodesApiService } from 'ng2-alfresco-core';
+import { Component, DebugElement } from '@angular/core';
+import { Observable } from 'rxjs/Rx';
+
+import { AlfrescoApiService } from 'ng2-alfresco-core';
+import { ContentManagementService } from '../services/content-management.service';
+import { NodeFavoriteDirective } from './node-favorite.directive';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ selection;
+}
+
+describe('NodeFavoriteDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ let element: DebugElement;
+ let directiveInstance;
+ let apiService;
+ let contentService;
+ let favoritesApi;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule
+ ],
+ declarations: [
+ TestComponent,
+ NodeFavoriteDirective
+ ],
+ providers: [
+ ContentManagementService,
+ AlfrescoApiService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodeFavoriteDirective));
+ directiveInstance = element.injector.get(NodeFavoriteDirective);
+
+ contentService = TestBed.get(ContentManagementService);
+ apiService = TestBed.get(AlfrescoApiService);
+ favoritesApi = apiService.getInstance().core.favoritesApi;
+ });
+
+ describe('selection input change event', () => {
+ it('does not call markFavoritesNodes() if input list is empty', () => {
+ spyOn(directiveInstance, 'markFavoritesNodes');
+
+ component.selection = [];
+
+ fixture.detectChanges();
+
+ expect(directiveInstance.markFavoritesNodes).not.toHaveBeenCalledWith();
+ });
+
+ it('calls markFavoritesNodes() on input change', () => {
+ spyOn(directiveInstance, 'markFavoritesNodes');
+
+ component.selection = [{ entry: { id: '1', name: 'name1' } }];
+
+ fixture.detectChanges();
+
+ expect(directiveInstance.markFavoritesNodes).toHaveBeenCalledWith(component.selection);
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+
+ expect(directiveInstance.markFavoritesNodes).toHaveBeenCalledWith(component.selection);
+ });
+ });
+
+ describe('markFavoritesNodes()', () => {
+ let favoritesApiSpy;
+
+ beforeEach(() => {
+ favoritesApiSpy = spyOn(favoritesApi, 'getFavorite');
+ });
+
+ it('check each selected node if it is a favorite', fakeAsync(() => {
+ favoritesApiSpy.and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(favoritesApiSpy.calls.count()).toBe(2);
+ }));
+
+ it('it does not check processed node when another is unselected', fakeAsync(() => {
+ favoritesApiSpy.and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(2);
+ expect(favoritesApiSpy.calls.count()).toBe(2);
+
+ favoritesApiSpy.calls.reset();
+
+ component.selection = [
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(1);
+ expect(favoritesApiSpy).not.toHaveBeenCalled();
+ }));
+
+ it('it does not check processed nodes when another is selected', fakeAsync(() => {
+ favoritesApiSpy.and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(2);
+ expect(favoritesApiSpy.calls.count()).toBe(2);
+
+ favoritesApiSpy.calls.reset();
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } },
+ { entry: { id: '3', name: 'name3' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(3);
+ expect(favoritesApiSpy.calls.count()).toBe(1);
+ }));
+ });
+
+ describe('toggleFavorite()', () => {
+ let removeFavoriteSpy;
+ let addFavoriteSpy;
+
+ beforeEach(() => {
+ removeFavoriteSpy = spyOn(favoritesApi, 'removeFavoriteSite');
+ addFavoriteSpy = spyOn(favoritesApi, 'addFavorite');
+ });
+
+ it('does not perform action if favorites collection is empty', () => {
+ component.selection = [];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(removeFavoriteSpy).not.toHaveBeenCalled();
+ expect(addFavoriteSpy).not.toHaveBeenCalled();
+ });
+
+ it('calls addFavorite() if none is a favorite', fakeAsync(() => {
+ addFavoriteSpy.and.returnValue(Promise.resolve());
+
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: false } },
+ { entry: { id: '2', name: 'name2', isFavorite: false } }
+ ];
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(addFavoriteSpy.calls.argsFor(0)[1].length).toBe(2);
+ }));
+
+ it('calls addFavorite() on the node that is not a favorite in selection', fakeAsync(() => {
+ addFavoriteSpy.and.returnValue(Promise.resolve());
+
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFile: true, isFolder: false, isFavorite: false } },
+ { entry: { id: '2', name: 'name2', isFile: true, isFolder: false, isFavorite: true } }
+ ];
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ const callArgs = addFavoriteSpy.calls.argsFor(0)[1];
+ const callParameter = callArgs[0];
+
+ expect(callArgs.length).toBe(1);
+ expect(callParameter.target.file.guid).toBe('1');
+ }));
+
+ it('calls removeFavoriteSite() if all are favorites', fakeAsync(() => {
+ addFavoriteSpy.and.returnValue(Promise.resolve());
+
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: true } },
+ { entry: { id: '2', name: 'name2', isFavorite: true } }
+ ];
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(removeFavoriteSpy.calls.count()).toBe(2);
+ }));
+ });
+
+ describe('getFavorite()', () => {
+ it('process node as favorite', fakeAsync(() => {
+ spyOn(favoritesApi, 'getFavorite').and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites[0].entry.isFavorite).toBe(true);
+ }));
+
+ it('process node as not a favorite', fakeAsync(() => {
+ spyOn(favoritesApi, 'getFavorite').and.returnValue(Promise.reject(null));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(directiveInstance.favorites[0].entry.isFavorite).toBe(false);
+ }));
+ });
+
+ describe('reset()', () => {
+ beforeEach(() => {
+ spyOn(favoritesApi, 'removeFavoriteSite').and.returnValue(Promise.resolve());
+ spyOn(favoritesApi, 'addFavorite').and.returnValue(Promise.resolve());
+ });
+
+ it('reset favorite collection after addFavorite()', fakeAsync(() => {
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: true } }
+ ];
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(0);
+ }));
+
+ it('reset favorite collection after removeFavoriteSite()', fakeAsync(() => {
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: false } }
+ ];
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(directiveInstance.favorites.length).toBe(0);
+ }));
+ });
+
+ describe('hasFavorites()', () => {
+ it('returns false if favorites collection is empty', () => {
+ directiveInstance.favorites = [];
+
+ const hasFavorites = directiveInstance.hasFavorites();
+
+ expect(hasFavorites).toBe(false);
+ });
+
+ it('returns false if some are not favorite', () => {
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: true } },
+ { entry: { id: '2', name: 'name2', isFavorite: false } }
+ ];
+
+ const hasFavorites = directiveInstance.hasFavorites();
+
+ expect(hasFavorites).toBe(false);
+ });
+
+ it('returns true if all are favorite', () => {
+ directiveInstance.favorites = [
+ { entry: { id: '1', name: 'name1', isFavorite: true } },
+ { entry: { id: '2', name: 'name2', isFavorite: true } }
+ ];
+
+ const hasFavorites = directiveInstance.hasFavorites();
+
+ expect(hasFavorites).toBe(true);
+ });
+ });
+});
diff --git a/src/app/common/directives/node-favorite.directive.ts b/src/app/common/directives/node-favorite.directive.ts
new file mode 100644
index 000000000..27460d1b1
--- /dev/null
+++ b/src/app/common/directives/node-favorite.directive.ts
@@ -0,0 +1,181 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input, OnChanges } from '@angular/core';
+import { AlfrescoApiService } from 'ng2-alfresco-core';
+
+import { MinimalNodeEntity, FavoriteBody } from 'alfresco-js-api';
+import { Observable } from 'rxjs/Rx';
+
+import { ContentManagementService } from '../services/content-management.service';
+
+@Directive({
+ selector: '[app-favorite-node]',
+ exportAs: 'favorite'
+})
+export class NodeFavoriteDirective implements OnChanges {
+ private favorites: any[] = [];
+
+ @Input('app-favorite-node')
+ selection: any[];
+
+ @HostListener('click')
+ onClick() {
+ this.toggleFavorite();
+ }
+
+ constructor(
+ public content: ContentManagementService,
+ private alfrescoApiService: AlfrescoApiService
+ ) {}
+
+ ngOnChanges(changes) {
+ if (!changes.selection.currentValue.length) {
+ return;
+ }
+
+ this.markFavoritesNodes(changes.selection.currentValue);
+ }
+
+ toggleFavorite() {
+ if (!this.favorites.length) {
+ return;
+ }
+
+ const every = this.favorites.every((selected) => selected.entry.isFavorite);
+
+ if (every) {
+ const batch = this.favorites.map((selected) => {
+ // shared files have nodeId
+ const id = selected.entry.nodeId || selected.entry.id;
+
+ return Observable.of(this.alfrescoApiService.getInstance().core.favoritesApi.removeFavoriteSite('-me-', id));
+ });
+
+ Observable.forkJoin(batch)
+ .subscribe(() => {
+ this.content.toggleFavorite.next();
+ this.reset();
+ });
+ }
+
+ if (!every) {
+ const notFavorite = this.favorites.filter((node) => !node.entry.isFavorite);
+ const body: FavoriteBody[] = notFavorite.map((node) => this.createFavoriteBody(node));
+
+ Observable.from(this.alfrescoApiService.getInstance().core.favoritesApi.addFavorite('-me-', body))
+ .subscribe(() => {
+ this.content.toggleFavorite.next();
+ this.reset();
+ });
+ }
+ }
+
+ markFavoritesNodes(selection) {
+ if (selection.length < this.favorites.length) {
+ const newFavorites = this.reduce(this.favorites, selection);
+ this.favorites = newFavorites;
+ }
+
+ const result = this.diff(selection, this.favorites);
+ const batch = this.getProcessBatch(result);
+
+ Observable.forkJoin(batch).subscribe((data) => this.favorites.push(...data));
+ }
+
+ hasFavorites(): boolean {
+ if (this.favorites && !this.favorites.length) {
+ return false;
+ }
+
+ return this.favorites.every((selected) => selected.entry.isFavorite);
+ }
+
+ private reset() {
+ this.favorites = [];
+ }
+
+ private getProcessBatch(selection): any[] {
+ return selection.map((selected) => this.getFavorite(selected));
+ }
+
+ private getFavorite(selected): Observable {
+ const { name, isFile, isFolder } = selected.entry;
+ // shared files have nodeId
+ const id = selected.entry.nodeId || selected.entry.id;
+
+ const promise = this.alfrescoApiService.getInstance()
+ .core.favoritesApi.getFavorite('-me-', id);
+
+ return Observable.from(promise)
+ .map(() => ({
+ entry: {
+ id,
+ isFolder,
+ isFile,
+ name,
+ isFavorite: true
+ }
+ }))
+ .catch(() => {
+ return Observable.of({
+ entry: {
+ id,
+ isFolder,
+ isFile,
+ name,
+ isFavorite: false
+ }
+ });
+ });
+ }
+
+ private createFavoriteBody(node): FavoriteBody {
+ const type = this.getNodeType(node);
+ // shared files have nodeId
+ const id = node.entry.nodeId || node.entry.id;
+
+ return {
+ target: {
+ [type]: {
+ guid: id
+ }
+ }
+ };
+ }
+
+ private getNodeType(node): string {
+ // shared could only be files
+ if (!node.entry.isFile && !node.entry.isFolder) {
+ return 'file';
+ }
+
+ return node.entry.isFile ? 'file' : 'folder';
+ }
+
+ private diff(list, patch): any[] {
+ const ids = patch.map(item => item.entry.id);
+
+ return list.filter(item => ids.includes(item.entry.id) ? null : item);
+ }
+
+ private reduce(patch, comparator): any[] {
+ const ids = comparator.map(item => item.entry.id);
+
+ return patch.filter(item => ids.includes(item.entry.id) ? item : null);
+ }
+}
diff --git a/src/app/common/directives/node-move.directive.spec.ts b/src/app/common/directives/node-move.directive.spec.ts
new file mode 100644
index 000000000..78f3856cd
--- /dev/null
+++ b/src/app/common/directives/node-move.directive.spec.ts
@@ -0,0 +1,299 @@
+import { Component, DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { Observable } from 'rxjs/Rx';
+
+import { CoreModule, TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { DocumentListModule } from 'ng2-alfresco-documentlist';
+
+import { NodeActionsService } from '../services/node-actions.service';
+import { ContentManagementService } from '../services/content-management.service';
+import { NodeMoveDirective } from './node-move.directive';
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ selection;
+}
+
+describe('NodeMoveDirective', () => {
+ let fixture: ComponentFixture;
+ let component: TestComponent;
+ let element: DebugElement;
+ let notificationService: NotificationService;
+ let nodesApiService: NodesApiService;
+ let service: NodeActionsService;
+ let translationService: TranslationService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ DocumentListModule
+ ],
+ declarations: [
+ TestComponent,
+ NodeMoveDirective
+ ],
+ providers: [
+ ContentManagementService,
+ NodeActionsService
+ ]
+ });
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodeMoveDirective));
+ notificationService = TestBed.get(NotificationService);
+ nodesApiService = TestBed.get(NodesApiService);
+ service = TestBed.get(NodeActionsService);
+ translationService = TestBed.get(TranslationService);
+ });
+
+ beforeEach(() => {
+ spyOn(translationService, 'get').and.callFake((keysArray) => {
+ const processedKeys = {};
+ keysArray.forEach((key) => {
+ processedKeys[key] = key;
+ });
+ return Observable.of(processedKeys);
+ });
+ });
+
+ describe('Move node action', () => {
+ beforeEach(() => {
+ spyOn(notificationService, 'openSnackMessageAction').and.callThrough();
+ });
+
+ it('notifies successful move of a node', () => {
+ const node = [ { entry: { id: 'node-to-move-id', name: 'name' } } ];
+ const moveResponse = {
+ succeeded: node,
+ failed: [],
+ partiallySucceeded: []
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = node;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies successful move of multiple nodes', () => {
+ const nodes = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }];
+ const moveResponse = {
+ succeeded: nodes,
+ failed: [],
+ partiallySucceeded: []
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = nodes;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.PLURAL', 'Undo', 10000
+ );
+ });
+
+ it('notifies partial move of a node', () => {
+ const node = [ { entry: { id: '1', name: 'name' } } ];
+ const moveResponse = {
+ succeeded: [],
+ failed: [],
+ partiallySucceeded: node
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = node;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies partial move of multiple nodes', () => {
+ const nodes = [
+ { entry: { id: '1', name: 'name' } },
+ { entry: { id: '2', name: 'name2' } } ];
+ const moveResponse = {
+ succeeded: [],
+ failed: [],
+ partiallySucceeded: nodes
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = nodes;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL', 'Undo', 10000
+ );
+ });
+
+ it('notifies successful move and the number of nodes that could not be moved', () => {
+ const nodes = [ { entry: { id: '1', name: 'name' } },
+ { entry: { id: '2', name: 'name2' } } ];
+ const moveResponse = {
+ succeeded: [ nodes[0] ],
+ failed: [ nodes[1] ],
+ partiallySucceeded: []
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = nodes;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL', 'Undo', 10000
+ );
+ });
+
+ it('notifies successful move and the number of partially moved ones', () => {
+ const nodes = [ { entry: { id: '1', name: 'name' } },
+ { entry: { id: '2', name: 'name2' } } ];
+ const moveResponse = {
+ succeeded: [ nodes[0] ],
+ failed: [],
+ partiallySucceeded: [ nodes[1] ]
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = nodes;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR', 'Undo', 10000
+ );
+ });
+
+ it('notifies error if success message was not emitted', () => {
+ const node = { entry: { id: 'node-to-move-id', name: 'name' } };
+ const moveResponse = {
+ succeeded: [],
+ failed: [],
+ partiallySucceeded: []
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of(''));
+
+ component.selection = [ node ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC', '', 3000);
+ });
+
+ it('notifies permission error on move of node', () => {
+ spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
+
+ component.selection = [{ entry: { id: '1', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.PERMISSION', '', 3000
+ );
+ });
+
+ it('notifies generic error message on all errors, but 403', () => {
+ spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
+
+ component.selection = [{ entry: { id: '1', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.GENERIC', '', 3000
+ );
+ });
+
+ it('notifies conflict error message on 409', () => {
+ spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}}))));
+
+ component.selection = [{ entry: { id: '1', name: 'name' } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.NODE_MOVE', '', 3000
+ );
+ });
+
+ it('notifies error if move response has only failed items', () => {
+ const node = [ { entry: { id: '1', name: 'name' } } ];
+ const moveResponse = {
+ succeeded: [],
+ failed: [ {} ],
+ partiallySucceeded: []
+ };
+
+ spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
+ spyOn(service, 'processResponse').and.returnValue(moveResponse);
+
+ component.selection = node;
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ service.contentMoved.next(moveResponse);
+
+ expect(service.moveNodes).toHaveBeenCalled();
+ expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.GENERIC', '', 3000
+ );
+ });
+ });
+});
diff --git a/src/app/common/directives/node-move.directive.ts b/src/app/common/directives/node-move.directive.ts
new file mode 100644
index 000000000..c329b1fb3
--- /dev/null
+++ b/src/app/common/directives/node-move.directive.ts
@@ -0,0 +1,210 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+
+import { TranslationService, NodesApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+
+import { ContentManagementService } from '../services/content-management.service';
+import { NodeActionsService } from '../services/node-actions.service';
+import { Observable } from 'rxjs/Rx';
+
+@Directive({
+ selector: '[app-move-node]'
+})
+
+export class NodeMoveDirective {
+ @Input('app-move-node')
+ selection: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.moveSelected();
+ }
+
+ constructor(
+ private content: ContentManagementService,
+ private notification: NotificationService,
+ private nodeActionsService: NodeActionsService,
+ private nodesApi: NodesApiService,
+ private translation: TranslationService
+ ) {}
+
+ moveSelected() {
+ const permissionForMove: string = 'delete';
+
+ Observable.zip(
+ this.nodeActionsService.moveNodes(this.selection, permissionForMove),
+ this.nodeActionsService.contentMoved
+ ).subscribe(
+ (result) => {
+ const [ operationResult, moveResponse ] = result;
+ this.toastMessage(operationResult, moveResponse);
+
+ this.content.moveNode.next(null);
+ },
+ (error) => {
+ this.toastMessage(error);
+ }
+ );
+ }
+
+ private toastMessage(info: any, moveResponse?: any) {
+ const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0;
+ const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0;
+ const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0;
+
+ let successMessage = '';
+ let partialSuccessMessage = '';
+ let failedMessage = '';
+ let errorMessage = '';
+
+ if (typeof info === 'string') {
+
+ // in case of success
+ if (info.toLowerCase().indexOf('succes') !== -1) {
+ let i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.';
+ let i18MessageSuffix = '';
+
+ if (succeeded) {
+ i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL';
+ successMessage = `${i18nMessageString}${i18MessageSuffix}`;
+ }
+
+ if (partiallySucceeded) {
+ i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL';
+ partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`;
+ }
+
+ if (failures) {
+ // if moving failed for ALL nodes, emit error
+ if (failures === this.selection.length) {
+ const errors = this.nodeActionsService.flatten(moveResponse['failed']);
+ errorMessage = this.getErrorMessage(errors[0]);
+
+ } else {
+ i18MessageSuffix = 'PARTIAL.FAIL';
+ failedMessage = `${i18nMessageString}${i18MessageSuffix}`;
+ }
+ }
+ } else {
+ errorMessage = 'APP.MESSAGES.ERRORS.GENERIC';
+ }
+
+ } else {
+ errorMessage = this.getErrorMessage(info);
+ }
+
+ const undo = (succeeded + partiallySucceeded > 0) ? 'Undo' : '';
+ const withUndo = errorMessage ? '' : '_WITH_UNDO';
+ failedMessage = errorMessage ? errorMessage : failedMessage;
+
+ const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : '';
+ const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : '';
+
+ const initialParentId = this.nodeActionsService.getFirstParentId(this.selection);
+
+ this.translation.get(
+ [successMessage, partialSuccessMessage, failedMessage],
+ { success: succeeded, failed: failures, partially: partiallySucceeded}).subscribe(messages => {
+
+ this.notification.openSnackMessageAction(
+ messages[successMessage]
+ + beforePartialSuccessMessage + messages[partialSuccessMessage]
+ + beforeFailedMessage + messages[failedMessage],
+ undo,
+ NodeActionsService[`SNACK_MESSAGE_DURATION${withUndo}`]
+ )
+ .onAction()
+ .subscribe(() => this.revertMoving(moveResponse, initialParentId));
+ });
+ }
+
+ getErrorMessage(errorObject): string {
+ let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
+
+ try {
+ const { error: { statusCode } } = JSON.parse(errorObject.message);
+
+ if (statusCode === 409) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE';
+
+ } else if (statusCode === 403) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
+ }
+
+ } catch (err) { /* Do nothing, keep the original message */ }
+
+ return i18nMessageString;
+ }
+
+ private revertMoving(moveResponse, selectionParentId) {
+ const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : [];
+ const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : [];
+
+ const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries
+ .map((folderEntry) => {
+ return this.nodesApi.restoreNode(folderEntry.nodeId || folderEntry.id)
+ .catch((err) => Observable.of(err));
+ });
+
+ Observable.zip(...restoreDeletedNodesBatch, Observable.of(null))
+ .flatMap(() => {
+
+ const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes];
+
+ const revertMoveBatch = this.nodeActionsService
+ .flatten(nodesToBeMovedBack)
+ .filter(node => node.entry || (node.itemMoved && node.itemMoved.entry))
+ .map((node) => {
+ if (node.itemMoved) {
+ return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId);
+ } else {
+ return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId);
+ }
+ });
+
+ return Observable.zip(...revertMoveBatch, Observable.of(null));
+ })
+ .subscribe(
+ () => {
+ this.content.moveNode.next(null);
+ },
+ (error) => {
+
+ let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
+
+ let errorJson = null;
+ try {
+ errorJson = JSON.parse(error.message);
+ } catch (e) { //
+ }
+
+ if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
+ i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
+ }
+
+ this.translation.get(i18nMessageString).subscribe(message => {
+ this.notification.openSnackMessage(
+ message, NodeActionsService.SNACK_MESSAGE_DURATION);
+ });
+ }
+ );
+ }
+
+}
diff --git a/src/app/common/directives/node-permanent-delete.directive.spec.ts b/src/app/common/directives/node-permanent-delete.directive.spec.ts
new file mode 100644
index 000000000..9bab26f07
--- /dev/null
+++ b/src/app/common/directives/node-permanent-delete.directive.spec.ts
@@ -0,0 +1,339 @@
+/*!
+ * @license
+ * Copyright 2017 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, DebugElement } from '@angular/core';
+import { TestBed, ComponentFixture, async, fakeAsync, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Observable } from 'rxjs/Rx';
+import { CoreModule, AlfrescoApiService, TranslationService, NotificationService } from 'ng2-alfresco-core';
+
+import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
+
+@Component({
+ template: ``
+})
+class TestComponent {
+ selection = [];
+}
+
+describe('NodePermanentDeleteDirective', () => {
+ let fixture: ComponentFixture;
+ let element: DebugElement;
+ let component: TestComponent;
+ let alfrescoService: AlfrescoApiService;
+ let translation: TranslationService;
+ let notificationService: NotificationService;
+ let nodesService;
+ let directiveInstance;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule
+ ],
+ declarations: [
+ TestComponent,
+ NodePermanentDeleteDirective
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective));
+ directiveInstance = element.injector.get(NodePermanentDeleteDirective);
+
+ alfrescoService = TestBed.get(AlfrescoApiService);
+ nodesService = alfrescoService.getInstance().nodes;
+ translation = TestBed.get(TranslationService);
+ notificationService = TestBed.get(NotificationService);
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(translation, 'get').and.returnValue(Observable.of('message'));
+ spyOn(notificationService, 'openSnackMessage').and.returnValue({});
+ });
+
+ it('does not purge nodes if no selection', () => {
+ spyOn(nodesService, 'purgeDeletedNode');
+
+ component.selection = [];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(nodesService.purgeDeletedNode).not.toHaveBeenCalled();
+ });
+
+ it('call purge nodes if selection is not empty', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.resolve());
+
+ component.selection = [ { entry: { id: '1' } } ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(nodesService.purgeDeletedNode).toHaveBeenCalled();
+ }));
+
+ describe('notification', () => {
+ it('notifies on multiple fail and one success', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.resolve();
+ }
+
+ if (id === '2') {
+ return Promise.reject({});
+ }
+
+ if (id === '3') {
+ return Promise.reject({});
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } },
+ { entry: { id: '3', name: 'name3' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_SINGULAR',
+ { name: 'name1', failed: 2 }
+ );
+ }));
+
+ it('notifies on multiple success and multiple fail', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.resolve();
+ }
+
+ if (id === '2') {
+ return Promise.reject({});
+ }
+
+ if (id === '3') {
+ return Promise.reject({});
+ }
+
+ if (id === '4') {
+ return Promise.resolve();
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } },
+ { entry: { id: '3', name: 'name3' } },
+ { entry: { id: '4', name: 'name4' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_PLURAL',
+ { number: 2, failed: 2 }
+ );
+ }));
+
+ it('notifies on one selected node success', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.SINGULAR',
+ { name: 'name1' }
+ );
+ }));
+
+ it('notifies on one selected node fail', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.reject({}));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.SINGULAR',
+ { name: 'name1' }
+ );
+ }));
+
+ it('notifies on selected nodes success', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.resolve();
+ }
+
+ if (id === '2') {
+ return Promise.resolve();
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PLURAL',
+ { number: 2 }
+ );
+ }));
+
+ it('notifies on selected nodes fail', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.reject({});
+ }
+
+ if (id === '2') {
+ return Promise.reject({});
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(notificationService.openSnackMessage).toHaveBeenCalled();
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.PLURAL',
+ { number: 2 }
+ );
+ }));
+ });
+
+ describe('refresh()', () => {
+ it('resets selection on success', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(directiveInstance.selection).toEqual([]);
+ }));
+
+ it('resets selection on error', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.reject({}));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(directiveInstance.selection).toEqual([]);
+ }));
+
+ it('resets status', fakeAsync(() => {
+ const status = directiveInstance.processStatus([
+ { status: 0 },
+ { status: 1 }
+ ]);
+
+ expect(status.fail.length).toBe(1);
+ expect(status.success.length).toBe(1);
+
+ status.reset();
+
+ expect(status.fail.length).toBe(0);
+ expect(status.success.length).toBe(0);
+ }));
+
+ it('dispatch event on partial success', fakeAsync(() => {
+ spyOn(element.nativeElement, 'dispatchEvent');
+ spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.reject({});
+ }
+
+ if (id === '2') {
+ return Promise.resolve();
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } },
+ { entry: { id: '2', name: 'name2' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(element.nativeElement.dispatchEvent).toHaveBeenCalled();
+ }));
+
+ it('does not dispatch event on error', fakeAsync(() => {
+ spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.reject({}));
+ spyOn(element.nativeElement, 'dispatchEvent');
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1' } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(element.nativeElement.dispatchEvent).not.toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/common/directives/node-permanent-delete.directive.ts b/src/app/common/directives/node-permanent-delete.directive.ts
new file mode 100644
index 000000000..8c54da68d
--- /dev/null
+++ b/src/app/common/directives/node-permanent-delete.directive.ts
@@ -0,0 +1,194 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+import { Observable } from 'rxjs/Rx';
+
+import { TranslationService, AlfrescoApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntity, DeletedNodeEntry, PathInfoEntity } from 'alfresco-js-api';
+
+@Directive({
+ selector: '[app-permanent-delete-node]'
+})
+export class NodePermanentDeleteDirective {
+
+ @Input('app-permanent-delete-node')
+ selection: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.purge();
+ }
+
+ constructor(
+ private alfrescoApiService: AlfrescoApiService,
+ private translation: TranslationService,
+ private notification: NotificationService,
+ private el: ElementRef
+ ) {}
+
+ private purge() {
+ if (!this.selection.length) {
+ return;
+ }
+
+ const batch = this.getPurgedNodesBatch(this.selection);
+
+ Observable.forkJoin(batch)
+ .subscribe(
+ (purgedNodes) => {
+ const status = this.processStatus(purgedNodes);
+
+ this.purgeNotification(status);
+
+ if (status.success.length) {
+ this.emitDone();
+ }
+
+ this.selection = [];
+ status.reset();
+ }
+ );
+ }
+
+ private getPurgedNodesBatch(selection): Observable {
+ return selection.map((node: MinimalNodeEntity) => this.purgeDeletedNode(node));
+ }
+
+ private purgeDeletedNode(node): Observable {
+ const { id, name } = node.entry;
+ const promise = this.alfrescoApiService.getInstance().nodes.purgeDeletedNode(id);
+
+ return Observable.from(promise)
+ .map(() => ({
+ status: 1,
+ id,
+ name
+ }))
+ .catch((error) => {
+ return Observable.of({
+ status: 0,
+ id,
+ name
+ });
+ });
+ }
+
+ private purgeNotification(status): void {
+ this.getPurgeMessage(status)
+ .subscribe((message) => {
+ this.notification.openSnackMessage(message, 3000);
+ });
+ }
+
+ private getPurgeMessage(status): Observable {
+ if (status.oneSucceeded && status.someFailed && !status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_SINGULAR',
+ {
+ name: status.success[0].name,
+ failed: status.fail.length
+ }
+ );
+ }
+
+ if (status.someSucceeded && !status.oneSucceeded && status.someFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_PLURAL',
+ {
+ number: status.success.length,
+ failed: status.fail.length
+ }
+ );
+ }
+
+ if (status.oneSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.SINGULAR',
+ { name: status.success[0].name }
+ );
+ }
+
+ if (status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.SINGULAR',
+ { name: status.fail[0].name }
+ );
+ }
+
+ if (status.allSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PLURAL',
+ { number: status.success.length }
+ );
+ }
+
+ if (status.allFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.PLURAL',
+ { number: status.fail.length }
+ );
+ }
+ }
+
+ private processStatus(data = []): any {
+ const status = {
+ fail: [],
+ success: [],
+ get someFailed() {
+ return !!(this.fail.length);
+ },
+ get someSucceeded() {
+ return !!(this.success.length);
+ },
+ get oneFailed() {
+ return this.fail.length === 1;
+ },
+ get oneSucceeded() {
+ return this.success.length === 1;
+ },
+ get allSucceeded() {
+ return this.someSucceeded && !this.someFailed;
+ },
+ get allFailed() {
+ return this.someFailed && !this.someSucceeded;
+ },
+ reset() {
+ this.fail = [];
+ this.success = [];
+ }
+ };
+
+ return data.reduce(
+ (acc, node) => {
+ if (node.status) {
+ acc.success.push(node);
+ } else {
+ acc.fail.push(node);
+ }
+
+ return acc;
+ },
+ status
+ );
+ }
+
+ private emitDone() {
+ const e = new CustomEvent('selection-node-deleted', { bubbles: true });
+ this.el.nativeElement.dispatchEvent(e);
+ }
+}
diff --git a/src/app/common/directives/node-restore.directive.spec.ts b/src/app/common/directives/node-restore.directive.spec.ts
new file mode 100644
index 000000000..3a282a53c
--- /dev/null
+++ b/src/app/common/directives/node-restore.directive.spec.ts
@@ -0,0 +1,334 @@
+/*!
+ * @license
+ * Copyright 2017 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, DebugElement } from '@angular/core';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, ComponentFixture, async, fakeAsync, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Observable } from 'rxjs/Rx';
+import { CoreModule, AlfrescoApiService, TranslationService, NotificationService } from 'ng2-alfresco-core';
+
+import { NodeRestoreDirective } from './node-restore.directive';
+
+@Component({
+ template: ``
+})
+class TestComponent {
+ selection = [];
+}
+
+describe('NodeRestoreDirective', () => {
+ let fixture: ComponentFixture;
+ let element: DebugElement;
+ let component: TestComponent;
+ let alfrescoService: AlfrescoApiService;
+ let translation: TranslationService;
+ let notificationService: NotificationService;
+ let router: Router;
+ let nodesService;
+ let coreApi;
+ let directiveInstance;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule
+ ],
+ declarations: [
+ TestComponent,
+ NodeRestoreDirective
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
+ directiveInstance = element.injector.get(NodeRestoreDirective);
+
+ alfrescoService = TestBed.get(AlfrescoApiService);
+ nodesService = alfrescoService.getInstance().nodes;
+ coreApi = alfrescoService.getInstance().core;
+ translation = TestBed.get(TranslationService);
+ notificationService = TestBed.get(NotificationService);
+ router = TestBed.get(Router);
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(translation, 'get').and.returnValue(Observable.of('message'));
+ });
+
+ it('does not restore nodes if no selection', () => {
+ spyOn(nodesService, 'restoreNode');
+
+ component.selection = [];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(nodesService.restoreNode).not.toHaveBeenCalled();
+ });
+
+ it('does not restore nodes if selection has nodes without path', () => {
+ spyOn(nodesService, 'restoreNode');
+
+ component.selection = [ { entry: { id: '1' } } ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+
+ expect(nodesService.restoreNode).not.toHaveBeenCalled();
+ });
+
+ it('call restore nodes if selection has nodes with path', fakeAsync(() => {
+ spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
+ spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
+ list: { entries: [] }
+ }));
+
+ component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(nodesService.restoreNode).toHaveBeenCalled();
+ }));
+
+ describe('refresh()', () => {
+ it('reset selection', fakeAsync(() => {
+ spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
+ spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
+ list: { entries: [] }
+ }));
+
+ component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
+
+ fixture.detectChanges();
+
+ expect(directiveInstance.selection.length).toBe(1);
+
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(directiveInstance.selection.length).toBe(0);
+ }));
+
+ it('reset status', fakeAsync(() => {
+ directiveInstance.restoreProcessStatus.fail = [{}];
+ directiveInstance.restoreProcessStatus.success = [{}];
+
+ directiveInstance.restoreProcessStatus.reset();
+
+ expect(directiveInstance.restoreProcessStatus.fail).toEqual([]);
+ expect(directiveInstance.restoreProcessStatus.success).toEqual([]);
+ }));
+
+ it('dispatch event on finish', fakeAsync(() => {
+ spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
+ spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
+ list: { entries: [] }
+ }));
+ spyOn(element.nativeElement, 'dispatchEvent');
+
+ component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(element.nativeElement.dispatchEvent).toHaveBeenCalled();
+ }));
+ });
+
+ describe('notification', () => {
+ beforeEach(() => {
+ spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({
+ list: { entries: [] }
+ }));
+ });
+
+ it('notifies on partial multiple fail ', fakeAsync(() => {
+ const error = { message: '{ "error": {} }' };
+
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+
+ spyOn(nodesService, 'restoreNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.resolve();
+ }
+
+ if (id === '2') {
+ return Promise.reject(error);
+ }
+
+ if (id === '3') {
+ return Promise.reject(error);
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
+ { entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } },
+ { entry: { id: '3', name: 'name3', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL',
+ { number: 2 }
+ );
+ }));
+
+ it('notifies fail when restored node exist, error 409', fakeAsync(() => {
+ const error = { message: '{ "error": { "statusCode": 409 } }' };
+
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS',
+ { name: 'name1' }
+ );
+ }));
+
+ it('notifies fail when restored node returns different statusCode', fakeAsync(() => {
+ const error = { message: '{ "error": { "statusCode": 404 } }' };
+
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC',
+ { name: 'name1' }
+ );
+ }));
+
+ it('notifies fail when restored node location is missing', fakeAsync(() => {
+ const error = { message: '{ "error": { } }' };
+
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error));
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING',
+ { name: 'name1' }
+ );
+ }));
+
+ it('notifies success when restore multiple nodes', fakeAsync(() => {
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+ spyOn(nodesService, 'restoreNode').and.callFake((id) => {
+ if (id === '1') {
+ return Promise.resolve();
+ }
+
+ if (id === '2') {
+ return Promise.resolve();
+ }
+ });
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
+ { entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL'
+ );
+ }));
+
+ it('notifies success when restore selected node', fakeAsync(() => {
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) });
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
+
+ component.selection = [
+ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(translation.get).toHaveBeenCalledWith(
+ 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR',
+ { name: 'name1' }
+ );
+ }));
+
+ it('navigate to restore selected node location onAction', fakeAsync(() => {
+ spyOn(router, 'navigate');
+ spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve());
+ spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.of({}) });
+
+ component.selection = [
+ {
+ entry: {
+ id: '1',
+ name: 'name1',
+ path: {
+ elements: ['somewhere-over-the-rainbow']
+ }
+ }
+ }
+ ];
+
+ fixture.detectChanges();
+ element.triggerEventHandler('click', null);
+ tick();
+
+ expect(router.navigate).toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/common/directives/node-restore.directive.ts b/src/app/common/directives/node-restore.directive.ts
new file mode 100644
index 000000000..a6f87fc80
--- /dev/null
+++ b/src/app/common/directives/node-restore.directive.ts
@@ -0,0 +1,259 @@
+/*!
+ * @license
+ * Copyright 2017 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, HostListener, Input } from '@angular/core';
+import { Router } from '@angular/router';
+import { Observable } from 'rxjs/Rx';
+
+import { TranslationService, AlfrescoApiService, NotificationService } from 'ng2-alfresco-core';
+import { MinimalNodeEntity, DeletedNodeEntry, PathInfoEntity } from 'alfresco-js-api';
+
+@Directive({
+ selector: '[app-restore-node]'
+})
+export class NodeRestoreDirective {
+ private restoreProcessStatus;
+
+ @Input('app-restore-node')
+ selection: MinimalNodeEntity[];
+
+ @HostListener('click')
+ onClick() {
+ this.restore(this.selection);
+ }
+
+ constructor(
+ private alfrescoApiService: AlfrescoApiService,
+ private translation: TranslationService,
+ private router: Router,
+ private notification: NotificationService,
+ private el: ElementRef
+ ) {
+ this.restoreProcessStatus = this.processStatus();
+ }
+
+ private restore(selection: any) {
+ if (!selection.length) {
+ return;
+ }
+
+ const nodesWithPath = this.getNodesWithPath(selection);
+
+ if (selection.length && !nodesWithPath.length) {
+ this.restoreProcessStatus.fail.push(...selection);
+ this.restoreNotification();
+ this.refresh();
+ return;
+ }
+
+ this.restoreNodesBatch(nodesWithPath)
+ .do((restoredNodes) => {
+ const status = this.processStatus(restoredNodes);
+
+ this.restoreProcessStatus.fail.push(...status.fail);
+ this.restoreProcessStatus.success.push(...status.success);
+ })
+ .flatMap(() => this.getDeletedNodes())
+ .subscribe(
+ (deletedNodesList: any) => {
+ const { entries: nodelist } = deletedNodesList.list;
+ const { fail: restoreErrorNodes } = this.restoreProcessStatus;
+ const selectedNodes = this.diff(restoreErrorNodes, selection, false);
+ const remainingNodes = this.diff(selectedNodes, nodelist);
+
+ if (!remainingNodes.length) {
+ this.restoreNotification();
+ this.refresh();
+ } else {
+ this.restore(remainingNodes);
+ }
+ }
+ );
+ }
+
+ private restoreNodesBatch(batch: MinimalNodeEntity[]): Observable {
+ return Observable.forkJoin(batch.map((node) => this.restoreNode(node)));
+ }
+
+ private getNodesWithPath(selection): MinimalNodeEntity[] {
+ return selection.filter((node) => node.entry.path);
+ }
+
+ private getDeletedNodes(): Observable {
+ const promise = this.alfrescoApiService.getInstance()
+ .core.nodesApi.getDeletedNodes({ include: [ 'path' ] });
+
+ return Observable.from(promise);
+ }
+
+ private restoreNode(node): Observable {
+ const { entry } = node;
+
+ const promise = this.alfrescoApiService.getInstance().nodes.restoreNode(entry.id);
+
+ return Observable.from(promise)
+ .map(() => ({
+ status: 1,
+ entry
+ }))
+ .catch((error) => {
+ const { statusCode } = (JSON.parse(error.message)).error;
+
+ return Observable.of({
+ status: 0,
+ statusCode,
+ entry
+ });
+ });
+ }
+
+ private navigateLocation(path: PathInfoEntity) {
+ const parent = path.elements[path.elements.length - 1];
+
+ this.router.navigate([ '/personal-files', parent.id ]);
+ }
+
+ private diff(selection , list, fromList = true): any {
+ const ids = selection.map(item => item.entry.id);
+
+ return list.filter(item => {
+ if (fromList) {
+ return ids.includes(item.entry.id) ? item : null;
+ } else {
+ return !ids.includes(item.entry.id) ? item : null;
+ }
+ });
+ }
+
+ private processStatus(data = []): any {
+ const status = {
+ fail: [],
+ success: [],
+ get someFailed() {
+ return !!(this.fail.length);
+ },
+ get someSucceeded() {
+ return !!(this.success.length);
+ },
+ get oneFailed() {
+ return this.fail.length === 1;
+ },
+ get oneSucceeded() {
+ return this.success.length === 1;
+ },
+ get allSucceeded() {
+ return this.someSucceeded && !this.someFailed;
+ },
+ get allFailed() {
+ return this.someFailed && !this.someSucceeded;
+ },
+ reset() {
+ this.fail = [];
+ this.success = [];
+ }
+ };
+
+ return data.reduce(
+ (acc, node) => {
+ if (node.status) {
+ acc.success.push(node);
+ } else {
+ acc.fail.push(node);
+ }
+
+ return acc;
+ },
+ status
+ );
+ }
+
+ private getRestoreMessage(): Observable {
+ const { restoreProcessStatus: status } = this;
+
+ if (status.someFailed && !status.oneFailed) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL',
+ {
+ number: status.fail.length
+ }
+ );
+ }
+
+ if (status.oneFailed && status.fail[0].statusCode) {
+ if (status.fail[0].statusCode === 409) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS',
+ {
+ name: status.fail[0].entry.name
+ }
+ );
+ } else {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC',
+ {
+ name: status.fail[0].entry.name
+ }
+ );
+ }
+ }
+
+ if (status.oneFailed && !status.fail[0].statusCode) {
+ return this.translation.get(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING',
+ {
+ name: status.fail[0].entry.name
+ }
+ );
+ }
+
+ if (status.allSucceeded && !status.oneSucceeded) {
+ return this.translation.get('APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL');
+ }
+
+ if (status.allSucceeded && status.oneSucceeded) {
+ return this.translation.get(
+ 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR',
+ {
+ name: status.success[0].entry.name
+ }
+ );
+ }
+ }
+
+ private restoreNotification(): void {
+ const status = Object.assign({}, this.restoreProcessStatus);
+ const action = (status.oneSucceeded && !status.someFailed) ? 'View' : '';
+
+ this.getRestoreMessage()
+ .subscribe((message) => {
+ this.notification.openSnackMessageAction(message, action, 3000)
+ .onAction()
+ .subscribe(() => this.navigateLocation(status.success[0].entry.path));
+ });
+ }
+
+ private refresh(): void {
+ this.restoreProcessStatus.reset();
+ this.selection = [];
+ this.emitDone();
+ }
+
+ private emitDone() {
+ const e = new CustomEvent('selection-node-restored', { bubbles: true });
+ this.el.nativeElement.dispatchEvent(e);
+ }
+}
diff --git a/src/app/common/material.module.ts b/src/app/common/material.module.ts
new file mode 100644
index 000000000..f4ac8de7b
--- /dev/null
+++ b/src/app/common/material.module.ts
@@ -0,0 +1,41 @@
+/*!
+ * @license
+ * Copyright 2017 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 { NgModule } from '@angular/core';
+import {
+ MdMenuModule,
+ MdIconModule,
+ MdButtonModule,
+ MdDialogModule,
+ MdInputModule
+} from '@angular/material';
+
+export function modules() {
+ return [
+ MdMenuModule,
+ MdIconModule,
+ MdButtonModule,
+ MdDialogModule,
+ MdInputModule
+ ];
+}
+
+@NgModule({
+ imports: modules(),
+ exports: modules()
+})
+export class MaterialModule {}
diff --git a/src/app/common/pipes/node-name-tooltip.pipe.spec.ts b/src/app/common/pipes/node-name-tooltip.pipe.spec.ts
new file mode 100644
index 000000000..aec9d63c3
--- /dev/null
+++ b/src/app/common/pipes/node-name-tooltip.pipe.spec.ts
@@ -0,0 +1,145 @@
+/*!
+ * @license
+ * Copyright 2017 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 { MinimalNodeEntity } from 'alfresco-js-api';
+import { NodeNameTooltipPipe } from './node-name-tooltip.pipe';
+
+describe('NodeNameTooltipPipe', () => {
+
+ const nodeName = 'node-name';
+ const nodeTitle = 'node-title';
+ const nodeDescription = 'node-description';
+
+ let pipe: NodeNameTooltipPipe;
+
+ beforeEach(() => {
+ pipe = new NodeNameTooltipPipe();
+ });
+
+ it('should not transform when missing node', () => {
+ expect(pipe.transform(null)).toBe(null);
+ });
+
+ it('should not transform when missing node entry', () => {
+ expect(pipe.transform( {})).toBe(null);
+ });
+
+ it('should use title and description when all fields present', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': nodeTitle,
+ 'cm:description': nodeDescription
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(`${nodeTitle}\n${nodeDescription}`);
+ });
+
+ it('should use name when other properties are missing', () => {
+ const node = {
+ entry: {
+ name: nodeName
+ }
+ };
+ let tooltip = pipe.transform( node);
+ expect(tooltip).toBe(nodeName);
+ });
+
+ it('should display name when title and description are missing', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {}
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(nodeName);
+ });
+
+ it('should use name and description when title is missing', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': null,
+ 'cm:description': nodeDescription
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(`${nodeName}\n${nodeDescription}`);
+ });
+
+ it('should use name and title when description is missing', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': nodeTitle,
+ 'cm:description': null
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(`${nodeName}\n${nodeTitle}`);
+ });
+
+ it('should use name if name and description are the same', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': null,
+ 'cm:description': nodeName
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(nodeName);
+ });
+
+ it('should use name if name and title are the same', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': nodeName,
+ 'cm:description': null
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(nodeName);
+ });
+
+ it('should use name if all values are the same', () => {
+ const node: any = {
+ entry: {
+ name: nodeName,
+ properties: {
+ 'cm:title': nodeName,
+ 'cm:description': nodeName
+ }
+ }
+ };
+ let tooltip = pipe.transform(node);
+ expect(tooltip).toBe(nodeName);
+ });
+});
diff --git a/src/app/common/pipes/node-name-tooltip.pipe.ts b/src/app/common/pipes/node-name-tooltip.pipe.ts
new file mode 100644
index 000000000..d1e5e398b
--- /dev/null
+++ b/src/app/common/pipes/node-name-tooltip.pipe.ts
@@ -0,0 +1,79 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Pipe, PipeTransform } from '@angular/core';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+import { DataRow } from 'ng2-alfresco-datatable';
+
+@Pipe({
+ name: 'nodeNameTooltip'
+})
+export class NodeNameTooltipPipe implements PipeTransform {
+
+ transform(node: MinimalNodeEntity): string {
+ if (node) {
+ return this.getNodeTooltip(node);
+ }
+ return null;
+ }
+
+ private containsLine(lines: string[], line: string): boolean {
+ return lines.some((item: string) => {
+ return item.toLowerCase() === line.toLowerCase();
+ });
+ }
+
+ private removeDuplicateLines(lines: string[]): string[] {
+ const reducer = (acc: string[], line: string): string[] => {
+ if (!this.containsLine(acc, line)) { acc.push(line); }
+ return acc;
+ };
+
+ return lines.reduce(reducer, []);
+ }
+
+ private getNodeTooltip(node: MinimalNodeEntity): string {
+ if (!node || !node.entry) {
+ return null;
+ }
+
+ const { entry: { properties, name } } = node;
+ const lines = [ name ];
+
+ if (properties) {
+ const {
+ 'cm:title': title,
+ 'cm:description': description
+ } = properties;
+
+ if (title && description) {
+ lines[0] = title;
+ lines[1] = description;
+ }
+
+ if (title) {
+ lines[1] = title;
+ }
+
+ if (description) {
+ lines[1] = description;
+ }
+ }
+
+ return this.removeDuplicateLines(lines).join(`\n`);
+ }
+}
diff --git a/src/app/common/services/browsing-files.service.spec.ts b/src/app/common/services/browsing-files.service.spec.ts
new file mode 100644
index 000000000..0d415ac40
--- /dev/null
+++ b/src/app/common/services/browsing-files.service.spec.ts
@@ -0,0 +1,36 @@
+/*!
+ * @license
+ * Copyright 2017 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 { BrowsingFilesService } from './browsing-files.service';
+
+describe('BrowsingFilesService', () => {
+ let service: BrowsingFilesService;
+
+ beforeEach(() => {
+ service = new BrowsingFilesService();
+ });
+
+ it('subscribs to event', () => {
+ const value = 'test-value';
+
+ service.onChangeParent.subscribe((result) => {
+ expect(result).toBe(value);
+ });
+
+ service.onChangeParent.next(value);
+ });
+});
diff --git a/src/app/common/services/browsing-files.service.ts b/src/app/common/services/browsing-files.service.ts
new file mode 100644
index 000000000..cca3528f7
--- /dev/null
+++ b/src/app/common/services/browsing-files.service.ts
@@ -0,0 +1,26 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Subject } from 'rxjs/Subject';
+import { Injectable } from '@angular/core';
+
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+@Injectable()
+export class BrowsingFilesService {
+ onChangeParent = new Subject();
+}
diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts
new file mode 100644
index 000000000..3fcc47199
--- /dev/null
+++ b/src/app/common/services/content-management.service.ts
@@ -0,0 +1,31 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Subject } from 'rxjs/Rx';
+import { Injectable } from '@angular/core';
+
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+@Injectable()
+export class ContentManagementService {
+ createFolder = new Subject();
+ editFolder = new Subject();
+ deleteNode = new Subject();
+ moveNode = new Subject();
+ restoreNode = new Subject();
+ toggleFavorite = new Subject();
+}
diff --git a/src/app/common/services/node-actions.service.spec.ts b/src/app/common/services/node-actions.service.spec.ts
new file mode 100644
index 000000000..2316a2723
--- /dev/null
+++ b/src/app/common/services/node-actions.service.spec.ts
@@ -0,0 +1,1156 @@
+import { TestBed, async } from '@angular/core/testing';
+import { MdDialog, OverlayModule } from '@angular/material';
+import { Observable } from 'rxjs/Rx';
+import { CoreModule, AlfrescoApiService, NodesApiService } from 'ng2-alfresco-core';
+import { DocumentListService, NodeMinimal, NodeMinimalEntry } from 'ng2-alfresco-documentlist';
+import { NodeActionsService } from './node-actions.service';
+
+class TestNode extends NodeMinimalEntry {
+ constructor(id?: string, isFile?: boolean, name?: string, permission?: string[]) {
+ super();
+ this.entry = new NodeMinimal();
+ this.entry.id = id || 'node-id';
+ this.entry.isFile = isFile;
+ this.entry.isFolder = !isFile;
+ this.entry.nodeType = isFile ? 'content' : 'folder';
+ this.entry.name = name;
+ if (permission) {
+ this.entry['allowableOperations'] = permission;
+ }
+ }
+}
+
+describe('NodeActionsService', () => {
+ const actionIsForbidden = true;
+ const isFile = true;
+ const folderDestinationId = 'folder-destination-id';
+ const fileId = 'file-to-be-copied-id';
+ const conflictError = new Error(JSON.stringify({error: {statusCode: 409}}));
+ const permissionError = new Error(JSON.stringify({error: {statusCode: 403}}));
+ const badRequestError = new Error(JSON.stringify({error: {statusCode: 400}}));
+ const emptyChildrenList = {list: {entries: []}};
+ let service: NodeActionsService;
+ let apiService: AlfrescoApiService;
+ let nodesApiService: NodesApiService;
+ let nodesApi;
+ let spyOnSuccess = jasmine.createSpy('spyOnSuccess');
+ let spyOnError = jasmine.createSpy('spyOnError');
+
+ let helper = {
+ fakeCopyNode: (isForbidden: boolean = false, nameExistingOnDestination?: string) => {
+ return (entryId, options) => {
+ return new Promise((resolve, reject) => {
+
+ if (isForbidden) {
+ reject(permissionError);
+
+ } else if (nameExistingOnDestination && options && options.name === nameExistingOnDestination) {
+
+ reject(conflictError);
+
+ } else {
+ resolve();
+ }
+ });
+ };
+ },
+ fakeGetNodeChildren: (familyNodes: { parentNodeId: string, nodeChildren: any[] }[], isForbidden: boolean = false) => {
+ return (parentId, options) => {
+
+ return new Promise((resolve, reject) => {
+ if (isForbidden) {
+ reject(permissionError);
+
+ } else {
+ const node = familyNodes.filter(familyNode => familyNode.parentNodeId === parentId);
+ resolve({list: {entries: node[0].nodeChildren}} || emptyChildrenList);
+ }
+ });
+
+ };
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ OverlayModule
+ ],
+ providers: [
+ MdDialog,
+ DocumentListService,
+ AlfrescoApiService,
+ NodesApiService,
+ NodeActionsService]
+ });
+
+ service = TestBed.get(NodeActionsService);
+ apiService = TestBed.get(AlfrescoApiService);
+ nodesApiService = TestBed.get(NodesApiService);
+ nodesApi = apiService.getInstance().nodes;
+ });
+
+ describe('doBatchOperation', () => {
+ beforeEach(() => {
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ });
+
+ it('should throw error if \'contentEntities\' required parameter is missing', async(() => {
+ let contentEntities;
+ const doCopyBatchOperation = service.copyNodes(contentEntities).asObservable();
+
+ doCopyBatchOperation.toPromise().then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnSuccess).not.toHaveBeenCalled();
+ expect(spyOnError).toHaveBeenCalled();
+ }
+ );
+ }));
+
+ it('should throw error if \'contentEntities\' is not an array of entry entities', async(() => {
+ const contentEntities = [ new TestNode(), {} ];
+ const doCopyBatchOperation = service.copyNodes(contentEntities).asObservable();
+
+ doCopyBatchOperation.toPromise().then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnSuccess).not.toHaveBeenCalled();
+ expect(spyOnError).toHaveBeenCalledWith(badRequestError);
+ }
+ );
+ }));
+
+ it('should throw error if an entry in \'contentEntities\' does not have id nor nodeId property', async(() => {
+ const contentEntities = [ new TestNode(), {entry: {}} ];
+ const doCopyBatchOperation = service.copyNodes(contentEntities).asObservable();
+
+ doCopyBatchOperation.toPromise().then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnSuccess).not.toHaveBeenCalled();
+ expect(spyOnError).toHaveBeenCalledWith(badRequestError);
+ }
+ );
+ }));
+
+ it('should not throw error if entry in \'contentEntities\' does not have id, but has nodeId property', async(() => {
+ const contentEntities = [ new TestNode(), {entry: {nodeId: '1234'}} ];
+
+ spyOn(service, 'getContentNodeSelection').and.returnValue(Observable.of([new TestNode().entry]));
+ spyOn(service, 'copyNodeAction').and.returnValue(Observable.of({}));
+
+ const doCopyBatchOperation = service.copyNodes(contentEntities).asObservable();
+
+ doCopyBatchOperation.toPromise().then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalledWith(badRequestError);
+ }
+ );
+
+ }));
+ });
+
+ describe('getFirstParentId', () => {
+ it('should give the parentId, if that exists on the node entry', () => {
+ const parentID = 'parent-id';
+ const contentEntities = [ {entry: {nodeId: '1234', parentId: parentID}} ];
+
+ expect(service.getFirstParentId(contentEntities)).toBe(parentID);
+ });
+
+ it('should give the last element in path property, if parentId is missing and path exists on the node entry', () => {
+ const firstParentId = 'parent-0-id';
+ const contentEntities = [ {entry: {nodeId: '1234', path: {elements: [ {id: 'parent-1-id'}, { id: firstParentId} ]} }} ];
+
+ expect(service.getFirstParentId(contentEntities)).toBe(firstParentId);
+ });
+
+ it('should give the id of the first node entry, if none of nodes has either parentId, or path properties', () => {
+ const nodeID = '1234';
+ const contentEntities = [ {entry: {id: nodeID}}, {entry: {id: `${nodeID}-2`}} ];
+
+ expect(service.getFirstParentId(contentEntities)).toBe(nodeID);
+ });
+
+ it('should give the nodeId of the first node entry, if none of nodes has either parentId, or path properties', () => {
+ const nodeID = '1234';
+ const contentEntities = [ {entry: {nodeId: nodeID}}, {entry: {id: `${nodeID}-2`}} ];
+
+ expect(service.getFirstParentId(contentEntities)).toBe(nodeID);
+ });
+ });
+
+ describe('getEntryParentId', () => {
+ it('should return the parentId, if that exists on the node entry', () => {
+ const parentID = 'parent-id';
+ const entry = {nodeId: '1234', parentId: parentID};
+
+ expect(service.getEntryParentId(entry)).toBe(parentID);
+ });
+
+ it('should give the last element in path property, if parentId is missing and path exists on the node entry', () => {
+ const firstParentId = 'parent-0-id';
+ const entry = {nodeId: '1234', path: {elements: [ {id: 'parent-1-id'}, { id: firstParentId} ]} };
+
+ expect(service.getEntryParentId(entry)).toBe(firstParentId);
+ });
+ });
+
+ describe('copyNodes', () => {
+ let fileToCopy;
+ let folderToCopy;
+ let destinationFolder;
+
+ beforeEach(() => {
+ fileToCopy = new TestNode(fileId, isFile, 'file-name');
+ folderToCopy = new TestNode();
+ destinationFolder = new TestNode(folderDestinationId);
+
+ });
+
+ it('should be called', () => {
+ const spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
+ spyOn(service, 'getContentNodeSelection').and.returnValue(Observable.of([destinationFolder.entry]));
+ spyOn(service, 'copyNodeAction').and.returnValue(Observable.of({}));
+
+ service.copyNodes([fileToCopy, folderToCopy]);
+ expect(spyOnBatchOperation.calls.count()).toEqual(1);
+ expect(spyOnBatchOperation).toHaveBeenCalledWith('copy', [fileToCopy, folderToCopy], undefined);
+ });
+
+ it('should use the ContentNodeSelectorComponentData object with custom rowFilter & imageResolver & title, when opening the destination picker', () => {
+ const spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
+ const spyOnDestinationPicker = spyOn(service, 'getContentNodeSelection').and.callThrough();
+ spyOn(service, 'getFirstParentId').and.returnValue('parent-id');
+
+ let testContentNodeSelectorComponentData;
+ const dialog = TestBed.get(MdDialog);
+ const spyOnDialog = spyOn(dialog, 'open').and.callFake((contentNodeSelectorComponent: any, data: any) => {
+ testContentNodeSelectorComponentData = data;
+ return {};
+ });
+
+ service.copyNodes([fileToCopy, folderToCopy]);
+
+ expect(spyOnBatchOperation).toHaveBeenCalledWith('copy', [fileToCopy, folderToCopy], undefined);
+ expect(spyOnDestinationPicker.calls.count()).toEqual(1);
+ expect(spyOnDialog.calls.count()).toEqual(1);
+
+ expect(testContentNodeSelectorComponentData).toBeDefined();
+ expect(testContentNodeSelectorComponentData.data.rowFilter({node: destinationFolder})).toBeDefined();
+ expect(testContentNodeSelectorComponentData.data.imageResolver({node: destinationFolder})).toBeDefined();
+ expect(testContentNodeSelectorComponentData.data.title).toBe('copy to ...');
+
+ destinationFolder.entry['allowableOperations'] = ['update'];
+ expect(testContentNodeSelectorComponentData.data.imageResolver({node: destinationFolder})).toBeDefined();
+ });
+
+ it('should use the ContentNodeSelectorComponentData object with file name in title', () => {
+ const spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
+ spyOn(service, 'getContentNodeSelection').and.callThrough();
+ spyOn(service, 'getFirstParentId').and.returnValue('parent-id');
+
+ let testContentNodeSelectorComponentData;
+ const dialog = TestBed.get(MdDialog);
+ spyOn(dialog, 'open').and.callFake((contentNodeSelectorComponent: any, data: any) => {
+ testContentNodeSelectorComponentData = data;
+ return {};
+ });
+
+ service.copyNodes([{entry: {id: 'entry-id', name: 'entry-name'}}]);
+
+ expect(spyOnBatchOperation).toHaveBeenCalled();
+ expect(testContentNodeSelectorComponentData).toBeDefined();
+ expect(testContentNodeSelectorComponentData.data.title).toBe('copy entry-name to ...');
+ });
+
+ it('should use the ContentNodeSelectorComponentData object without file name in title, if no name exists', () => {
+ const spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
+ spyOn(service, 'getContentNodeSelection').and.callThrough();
+ spyOn(service, 'getFirstParentId').and.returnValue('parent-id');
+
+ let testContentNodeSelectorComponentData;
+ const dialog = TestBed.get(MdDialog);
+ spyOn(dialog, 'open').and.callFake((contentNodeSelectorComponent: any, data: any) => {
+ testContentNodeSelectorComponentData = data;
+ return {};
+ });
+
+ service.copyNodes([{entry: {id: 'entry-id'}}]);
+
+ expect(spyOnBatchOperation).toHaveBeenCalled();
+ expect(testContentNodeSelectorComponentData).toBeDefined();
+ expect(testContentNodeSelectorComponentData.data.title).toBe('copy to ...');
+ });
+
+ });
+
+ describe('copyNodeAction', () => {
+
+ it('should copy one folder node to destination', () => {
+ spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode());
+
+ let folderToCopy = new TestNode();
+ let folderDestination = new TestNode(folderDestinationId);
+ service.copyNodeAction(folderToCopy.entry, folderDestination.entry.id);
+
+ expect(nodesApi.copyNode).toHaveBeenCalledWith(
+ folderToCopy.entry.id,
+ { targetParentId: folderDestination.entry.id, name: undefined }
+ );
+ });
+
+ it('should copy one file node to destination', () => {
+ spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode());
+
+ let fileToCopy = new TestNode(fileId, isFile, 'file-name');
+ let folderDestination = new TestNode(folderDestinationId);
+ service.copyNodeAction(fileToCopy.entry, folderDestination.entry.id);
+
+ expect(nodesApi.copyNode).toHaveBeenCalledWith(
+ fileToCopy.entry.id,
+ { targetParentId: folderDestination.entry.id, name: 'file-name' }
+ );
+ });
+
+ it('should fail to copy folder node if action is forbidden', async(() => {
+ spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode(actionIsForbidden));
+
+ let folderToCopy = new TestNode();
+ let folderDestination = new TestNode(folderDestinationId);
+
+ const spyContentAction = spyOn(service, 'copyContentAction').and.callThrough();
+ const spyFolderAction = spyOn(service, 'copyFolderAction').and.callThrough();
+ const copyObservable = service.copyNodeAction(folderToCopy.entry, folderDestination.entry.id);
+
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ copyObservable.toPromise().then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+
+ expect(spyContentAction.calls.count()).toEqual(0);
+ expect(spyFolderAction.calls.count()).toEqual(1);
+ expect(nodesApi.copyNode).toHaveBeenCalledWith(
+ folderToCopy.entry.id,
+ { targetParentId: folderDestination.entry.id, name: undefined }
+ );
+ }).then(() => {
+ expect(spyOnSuccess.calls.count()).toEqual(0);
+ expect(spyOnError.calls.count()).toEqual(1);
+ });
+ }));
+
+ it('should fail to copy file node if action is forbidden', async(() => {
+ spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode(actionIsForbidden));
+
+ const spyContentAction = spyOn(service, 'copyContentAction').and.callThrough();
+ const spyFolderAction = spyOn(service, 'copyFolderAction').and.callThrough();
+
+ let fileToCopy = new TestNode(fileId, isFile, 'test-name');
+ let folderDestination = new TestNode(folderDestinationId);
+ const copyObservable = service.copyNodeAction(fileToCopy.entry, folderDestination.entry.id);
+
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ copyObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+ })
+ .then(
+ () => {
+ expect(spyOnSuccess).not.toHaveBeenCalled();
+ expect(spyOnError).toHaveBeenCalled();
+
+ expect(spyContentAction).toHaveBeenCalled();
+ expect(spyFolderAction).not.toHaveBeenCalled();
+ expect(nodesApi.copyNode).toHaveBeenCalledWith(
+ fileToCopy.entry.id,
+ {targetParentId: folderDestination.entry.id, name: 'test-name'}
+ );
+ });
+ }));
+
+ it('should copy one file node to same destination and autoRename it', async(() => {
+ const alreadyExistingName = 'file-name';
+ spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode(!actionIsForbidden, alreadyExistingName));
+
+ const spyContentAction = spyOn(service, 'copyContentAction').and.callThrough();
+
+ let fileToCopy = new TestNode(fileId, isFile, 'file-name');
+ let folderDestination = new TestNode(folderDestinationId);
+ const copyObservable = service.copyNodeAction(fileToCopy.entry, folderDestination.entry.id);
+
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ copyObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+
+ })
+ .then(() => {
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+
+ expect(spyContentAction.calls.count()).toEqual(2);
+ expect(nodesApi.copyNode).toHaveBeenCalledWith(
+ fileToCopy.entry.id,
+ {targetParentId: folderDestination.entry.id, name: 'file-name-1'}
+ );
+ });
+ }));
+
+ describe('should copy content of folder-to-copy to folder with same name from destination folder', () => {
+ let folderToCopy;
+ let fileChildOfFolderToCopy;
+ let folderParentAndDestination;
+ let existingFolder;
+ let spy;
+ let spyOnContentAction;
+ let spyOnFolderAction;
+ let copyObservable;
+
+ beforeEach(() => {
+ folderToCopy = new TestNode('folder-to-copy-id', !isFile, 'conflicting-name');
+ fileChildOfFolderToCopy = new TestNode(fileId, isFile, 'file-name');
+
+ folderParentAndDestination = new TestNode(folderDestinationId);
+ existingFolder = new TestNode('existing-folder-id', !isFile, 'conflicting-name');
+
+ spy = spyOn(nodesApi, 'copyNode').and.callFake(helper.fakeCopyNode(!actionIsForbidden, 'conflicting-name'));
+
+ spyOnContentAction = spyOn(service, 'copyContentAction').and.callThrough();
+ spyOnFolderAction = spyOn(service, 'copyFolderAction').and.callThrough();
+
+ copyObservable = service.copyNodeAction(folderToCopy.entry, folderParentAndDestination.entry.id);
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ });
+
+ it('when folder to copy has a file as content', async(() => {
+ const testFamilyNodes = [
+ {
+ parentNodeId: folderToCopy.entry.id,
+ nodeChildren: [fileChildOfFolderToCopy]
+ }, {
+ parentNodeId: folderParentAndDestination.entry.id,
+ nodeChildren: [existingFolder]
+ }
+ ];
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes));
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(existingFolder));
+
+ copyObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+ })
+ .then(
+ () => {
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+
+ expect(spyOnContentAction).toHaveBeenCalled();
+ expect(spyOnFolderAction).toHaveBeenCalled();
+ expect(spy.calls.allArgs()).toEqual([
+ [folderToCopy.entry.id, {
+ targetParentId: folderParentAndDestination.entry.id,
+ name: 'conflicting-name'
+ }],
+ [fileChildOfFolderToCopy.entry.id, {
+ targetParentId: existingFolder.entry.id,
+ name: 'file-name'
+ }]
+ ]);
+ });
+ }));
+
+ it('when folder to copy is empty', async(() => {
+ const testFamilyNodes = [
+ {
+ parentNodeId: folderToCopy.entry.id,
+ nodeChildren: []
+ }, {
+ parentNodeId: folderParentAndDestination.entry.id,
+ nodeChildren: [existingFolder]
+ }
+ ];
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes));
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(existingFolder));
+
+ copyObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+
+ })
+ .then(
+ () => {
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+
+ expect(spyOnContentAction).not.toHaveBeenCalled();
+ expect(spyOnFolderAction).toHaveBeenCalled();
+ expect(spy.calls.allArgs()).toEqual([
+ [folderToCopy.entry.id, {
+ targetParentId: folderParentAndDestination.entry.id,
+ name: 'conflicting-name'
+ }]
+ ]);
+ });
+ }));
+
+ it('when folder to copy has another folder as child', async(() => {
+ const folderChild = new TestNode('folder-child-id');
+ const testFamilyNodes = [
+ {
+ parentNodeId: folderToCopy.entry.id,
+ nodeChildren: [folderChild]
+ }, {
+ parentNodeId: folderParentAndDestination.entry.id,
+ nodeChildren: [existingFolder]
+ }
+ ];
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes));
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(existingFolder));
+
+ copyObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ () => {
+ spyOnError();
+ })
+ .then(
+ () => {
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+
+ expect(spyOnContentAction).not.toHaveBeenCalled();
+ expect(spyOnFolderAction.calls.count()).toEqual(2);
+ expect(spy.calls.allArgs()).toEqual([
+ [folderToCopy.entry.id, {
+ targetParentId: folderParentAndDestination.entry.id,
+ name: 'conflicting-name'
+ }],
+ [folderChild.entry.id, {
+ targetParentId: existingFolder.entry.id,
+ name: undefined
+ }]
+ ]);
+ }
+ );
+ }));
+ });
+
+ });
+
+ describe('moveNodes', () => {
+ const permissionToMove = 'delete';
+ let fileToMove;
+ let folderToMove;
+ let destinationFolder;
+ let spyOnBatchOperation;
+ let spyOnDocumentListServiceAction;
+ let documentListService;
+
+ beforeEach(() => {
+ fileToMove = new TestNode('file-to-be-moved', isFile, 'file-name');
+ folderToMove = new TestNode('fid', !isFile, 'folder-name');
+ destinationFolder = new TestNode(folderDestinationId);
+
+ documentListService = TestBed.get(DocumentListService);
+ spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
+ });
+
+ it('should allow to select destination for nodes that have permission to be moved', () => {
+ const spyOnDestinationPicker = spyOn(service, 'getContentNodeSelection').and.returnValue(Observable.of([destinationFolder.entry]));
+ spyOn(service, 'moveContentAction').and.returnValue(Observable.of({}));
+ spyOn(service, 'moveFolderAction').and.returnValue(Observable.of({}));
+
+ fileToMove.entry['allowableOperations'] = [permissionToMove];
+ folderToMove.entry['allowableOperations'] = [permissionToMove];
+
+ service.moveNodes([fileToMove, folderToMove], permissionToMove);
+ expect(spyOnBatchOperation).toHaveBeenCalledWith('move', [fileToMove, folderToMove], permissionToMove);
+ expect(spyOnDestinationPicker).toHaveBeenCalled();
+ });
+
+ it('should not allow to select destination for nodes that do not have permission to be moved', () => {
+ const spyOnDestinationPicker = spyOn(service, 'getContentNodeSelection').and.returnValue(Observable.of([destinationFolder.entry]));
+
+ fileToMove.entry['allowableOperations'] = [];
+ folderToMove.entry['allowableOperations'] = [];
+
+ service.moveNodes([fileToMove, folderToMove], permissionToMove);
+ expect(spyOnBatchOperation).toHaveBeenCalledWith('move', [fileToMove, folderToMove], permissionToMove);
+ expect(spyOnDestinationPicker).not.toHaveBeenCalled();
+ });
+
+ it('should call the documentListService moveNode directly for moving a file that has permission to be moved', () => {
+ const spyOnDestinationPicker = spyOn(service, 'getContentNodeSelection').and.returnValue(Observable.of([destinationFolder.entry]));
+ fileToMove.entry['allowableOperations'] = [permissionToMove];
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and.returnValue(Observable.of([fileToMove]));
+ spyOn(service, 'moveNodeAction');
+
+ service.moveNodes([fileToMove], permissionToMove);
+ expect(service.moveNodeAction).not.toHaveBeenCalled();
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+ });
+
+ describe('moveContentAction', () => {
+
+ beforeEach(() => {
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ });
+
+ it('should not throw error on conflict, to be able to show message in case of partial move of files', async(() => {
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and
+ .returnValue(Observable.throw(conflictError));
+
+ const moveContentActionObservable = service.moveContentAction(fileToMove.entry, folderDestinationId);
+ moveContentActionObservable.toPromise()
+ .then(
+ (value) => {
+ spyOnSuccess(value);
+ },
+ (error) => {
+ spyOnError(error);
+
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalledWith(conflictError);
+ expect(spyOnError).not.toHaveBeenCalledWith(conflictError);
+ });
+ }));
+
+ it('should throw permission error in case it occurs', async(() => {
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and
+ .returnValue(Observable.throw(permissionError));
+
+ const moveContentActionObservable = service.moveContentAction(fileToMove.entry, folderDestinationId);
+ moveContentActionObservable.toPromise()
+ .then(
+ (value) => {
+ spyOnSuccess(value);
+ },
+ (error) => {
+ spyOnError(error);
+
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).not.toHaveBeenCalledWith(permissionError);
+ expect(spyOnError).toHaveBeenCalledWith(permissionError);
+ });
+ }));
+
+ it('in case of success, should return also the initial parent id of the moved node', async(() => {
+ const parentID = 'parent-id';
+ fileToMove.entry['parentId'] = parentID;
+ fileToMove.entry['allowableOperations'] = [permissionToMove];
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and
+ .returnValue(Observable.of(fileToMove));
+
+ const moveContentActionObservable = service.moveContentAction(fileToMove.entry, folderDestinationId);
+ moveContentActionObservable.toPromise()
+ .then(
+ (value) => {
+ spyOnSuccess(value);
+ },
+ (error) => {
+ spyOnError(error);
+
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalledWith({itemMoved: fileToMove, initialParentId: parentID});
+ expect(spyOnError).not.toHaveBeenCalledWith(permissionError);
+ });
+ }));
+
+ });
+
+ describe('moveFolderAction', () => {
+
+ beforeEach(() => {
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ });
+
+ it('should throw permission error in case it occurs on folder move', async(() => {
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and
+ .returnValue(Observable.throw(permissionError));
+
+ const moveFolderActionObservable = service.moveFolderAction(folderToMove.entry, folderDestinationId);
+ moveFolderActionObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).not.toHaveBeenCalled();
+ expect(spyOnError).toHaveBeenCalledWith(permissionError);
+ });
+ }));
+
+ it('should not throw error on conflict in case it occurs on folder move', async(() => {
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and
+ .returnValue(Observable.throw(conflictError));
+
+ const newDestination = new TestNode('new-destination', !isFile, folderToMove.entry.name);
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(newDestination));
+ spyOn(service, 'getNodeChildren').and.returnValue(Observable.of(emptyChildrenList));
+
+ const moveFolderActionObservable = service.moveFolderAction(folderToMove.entry, folderDestinationId);
+ moveFolderActionObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalledWith(conflictError);
+ });
+ }));
+
+ it('should try to move children nodes of a folder to already existing folder with same name', async(() => {
+ const parentFolderToMove = new TestNode('parent-folder', !isFile, 'conflicting-name');
+ spyOnDocumentListServiceAction = spyOn(documentListService, 'moveNode').and.callFake(
+ (contentEntryId, selectionId) => {
+ if (contentEntryId === parentFolderToMove.entry.id) {
+ return Observable.throw(conflictError);
+
+ }
+ return Observable.of({});
+ });
+ spyOn(service, 'moveContentAction').and.returnValue(Observable.of({}));
+
+ const newDestination = new TestNode('new-destination', !isFile, 'conflicting-name');
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(newDestination));
+ const childrenNodes = [ fileToMove, folderToMove ];
+ spyOn(service, 'getNodeChildren').and.returnValue(Observable.of( {list: {entries: childrenNodes}} ));
+
+ const moveFolderActionObservable = service.moveFolderAction(parentFolderToMove.entry, folderDestinationId);
+ moveFolderActionObservable.toPromise()
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDocumentListServiceAction).toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalledWith(conflictError);
+ });
+ }));
+
+ });
+
+ describe('moveNodeAction', () => {
+ describe('on moving folder to a destination where a folder with the same name exists', () => {
+ let parentFolderToMove;
+ let moveNodeActionPromise;
+ let spyOnDelete;
+
+ beforeEach(() => {
+ parentFolderToMove = new TestNode('parent-folder', !isFile, 'conflicting-name');
+ spyOnDelete = spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of(null));
+ });
+
+ afterEach(() => {
+ spyOnDelete.calls.reset();
+ spyOnSuccess.calls.reset();
+ spyOnError.calls.reset();
+ });
+
+ it('should take no extra delete action, if folder was moved to the same location', async(() => {
+ spyOn(service, 'moveFolderAction').and.returnValue(Observable.of(null));
+
+ parentFolderToMove.entry.parentId = folderDestinationId;
+ moveNodeActionPromise = service.moveNodeAction(parentFolderToMove.entry, folderDestinationId).toPromise();
+ moveNodeActionPromise
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDelete).not.toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+ });
+ }));
+
+ it('should take no extra delete action, if its children were partially moved', async(() => {
+ const movedChildrenNodes = [ fileToMove, folderToMove ];
+ spyOn(service, 'moveFolderAction').and.returnValue(Observable.of(movedChildrenNodes));
+ spyOn(service, 'processResponse').and.returnValue({
+ succeeded: [ fileToMove ],
+ failed: [ folderToMove ],
+ partiallySucceeded: []
+ });
+
+ parentFolderToMove.entry.parentId = `not-${folderDestinationId}`;
+ moveNodeActionPromise = service.moveNodeAction(parentFolderToMove.entry, folderDestinationId).toPromise();
+ moveNodeActionPromise
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDelete).not.toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+ });
+ }));
+
+ it('should take extra delete action, if all children nodes were successfully moved and folder is still on location', async(() => {
+ const movedChildrenNodes = [ fileToMove, folderToMove ];
+ spyOn(service, 'moveFolderAction').and.returnValue(Observable.of(movedChildrenNodes));
+ spyOn(service, 'processResponse').and.returnValue({
+ succeeded: [ movedChildrenNodes ],
+ failed: [],
+ partiallySucceeded: []
+ });
+ const folderOnLocation = parentFolderToMove;
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(folderOnLocation));
+
+ parentFolderToMove.entry.parentId = `not-${folderDestinationId}`;
+ moveNodeActionPromise = service.moveNodeAction(parentFolderToMove.entry, folderDestinationId).toPromise();
+ moveNodeActionPromise
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDelete).toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+ });
+ }));
+
+ it('should take no extra delete action, if folder is no longer on location', async(() => {
+ const movedChildrenNodes = [ fileToMove, folderToMove ];
+ spyOn(service, 'moveFolderAction').and.returnValue(Observable.of(movedChildrenNodes));
+ spyOn(service, 'processResponse').and.returnValue({
+ succeeded: [ movedChildrenNodes ],
+ failed: [],
+ partiallySucceeded: []
+ });
+ spyOn(service, 'getChildByName').and.returnValue(Observable.of(null));
+
+ parentFolderToMove.entry.parentId = `not-${folderDestinationId}`;
+ moveNodeActionPromise = service.moveNodeAction(parentFolderToMove.entry, folderDestinationId).toPromise();
+ moveNodeActionPromise
+ .then(
+ () => {
+ spyOnSuccess();
+ },
+ (error) => {
+ spyOnError(error);
+ })
+ .then(() => {
+ expect(spyOnDelete).not.toHaveBeenCalled();
+
+ expect(spyOnSuccess).toHaveBeenCalled();
+ expect(spyOnError).not.toHaveBeenCalled();
+ });
+
+ }));
+ });
+ });
+
+ });
+
+ describe('getChildByName', () => {
+ let testFamilyNodes;
+ let notChildNode;
+ let childNode;
+
+ beforeEach(() => {
+ childNode = new TestNode(fileId, isFile, 'child-name');
+ let parentNode = new TestNode();
+
+ notChildNode = new TestNode('not-child-id', !isFile, 'not-child-name');
+ testFamilyNodes = [
+ {
+ parentNodeId: parentNode.entry.id,
+ nodeChildren: [childNode]
+ }, {
+ parentNodeId: notChildNode.entry.id,
+ nodeChildren: []
+ }
+ ];
+ });
+
+ it('emits child node with specified name, when it exists in folder', () => {
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes));
+
+ service.getChildByName(testFamilyNodes[0].parentNodeId, childNode.entry.name)
+ .subscribe(
+ (value) => {
+ expect(value).toEqual(childNode);
+ }
+ );
+ });
+
+ it('emits null value when child with specified name is not found in folder', async(() => {
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes));
+
+ service.getChildByName(testFamilyNodes[0].parentNodeId, notChildNode.entry.name)
+ .subscribe(
+ (value) => {
+ expect(value).toEqual(null);
+ }
+ );
+ }));
+
+ it('emits error when permission error occurs', async(() => {
+ spyOn(nodesApi, 'getNodeChildren').and.callFake(helper.fakeGetNodeChildren(testFamilyNodes, actionIsForbidden));
+
+ service.getChildByName(testFamilyNodes[0].parentNodeId, notChildNode.entry.name)
+ .subscribe(
+ (value) => {
+ expect(value).toEqual(null);
+ }
+ );
+ }));
+ });
+
+ describe('getNewNameFrom', () => {
+ let testData = [
+ {
+ name: 'noExtension',
+ baseName: 'noExtension',
+ expected: 'noExtension-1'
+ }, {
+ name: 'withExtension.txt',
+ baseName: 'withExtension.txt',
+ expected: 'withExtension-1.txt'
+ }, {
+ name: 'with-lineStringSufix.txt',
+ baseName: 'with-lineStringSufix.txt',
+ expected: 'with-lineStringSufix-1.txt'
+ }, {
+ name: 'noExtension-1',
+ baseName: 'noExtension-1',
+ expected: 'noExtension-1-1'
+ }, {
+ name: 'with-lineNumberSufix-1.txt',
+ baseName: 'with-lineNumberSufix-1.txt',
+ expected: 'with-lineNumberSufix-1-1.txt'
+ }, {
+ name: 'with-lineNumberSufix.txt',
+ baseName: undefined,
+ expected: 'with-lineNumberSufix-1.txt'
+ }, {
+ name: 'noExtension-1',
+ baseName: 'noExtension',
+ expected: 'noExtension-2'
+ }, {
+ name: 'noExtension-7',
+ baseName: undefined,
+ expected: 'noExtension-8'
+ }, {
+ name: 'noExtension-007',
+ baseName: undefined,
+ expected: 'noExtension-007-1'
+ }
+ ];
+ testData.forEach((data) => {
+ it(`new name should be \'${data.expected}\' for given name: \'${data.name}\', and baseName: \'${data.baseName}\'`, () => {
+ const result = service.getNewNameFrom(data.name, data.baseName);
+ expect(result).toBe(data.expected);
+ });
+
+ });
+ });
+
+ describe('flatten', () => {
+ const testNode1 = new TestNode('node1-id', isFile, 'node1-name');
+ const testNode2 = new TestNode('node2-id', !isFile, 'node2-name');
+
+ let testData = [
+ {
+ nDimArray: [ testNode1 ],
+ expected: [ testNode1 ]
+ },
+ {
+ nDimArray: [ [ testNode1 ], [ testNode2 ] ],
+ expected: [ testNode1, testNode2 ]
+ },
+ {
+ nDimArray: [ [ [ [ testNode1 ] ], testNode2 ] ],
+ expected: [ testNode2, testNode1 ]
+ }
+ ];
+
+ testData.forEach((data) => {
+ it(`flattened array should be \'${data.expected}\' for given data: \'${data.nDimArray}\'`, () => {
+ const result = service.flatten(data.nDimArray);
+
+ expect(result.length).toBe(data.expected.length);
+ expect(JSON.stringify(result)).toEqual(JSON.stringify(data.expected));
+ });
+
+ });
+ });
+
+ describe('processResponse', () => {
+ const testNode1 = new TestNode('node1-id', isFile, 'node1-name');
+ const testNode2 = new TestNode('node2-id', !isFile, 'node2-name');
+
+ const parentID = 'patent-1-id';
+ testNode1.entry['parentId'] = parentID;
+
+ let testData = [
+ {
+ data: [ testNode1 ],
+ expected: {
+ succeeded: [ testNode1 ],
+ failed: [],
+ partiallySucceeded: []
+ }
+ }, {
+ data: [ [ {itemMoved: testNode1, initialParentId: parentID} ] ],
+ expected: {
+ succeeded: [ [ {itemMoved: testNode1, initialParentId: parentID} ] ],
+ failed: [],
+ partiallySucceeded: []
+ }
+ }, {
+ data: [ conflictError ],
+ expected: {
+ succeeded: [],
+ failed: [ conflictError ],
+ partiallySucceeded: []
+ }
+ }, {
+ data: [ conflictError, testNode2 ],
+ expected: {
+ succeeded: [ testNode2 ],
+ failed: [ conflictError ],
+ partiallySucceeded: []
+ }
+ }, {
+ data: [ conflictError, [ testNode2, conflictError] ],
+ expected: {
+ succeeded: [],
+ failed: [ conflictError ],
+ partiallySucceeded: [ [ testNode2, conflictError ] ]
+ }
+ }, {
+ data: [ conflictError, [ {}, conflictError] ],
+ expected: {
+ succeeded: [],
+ failed: [ conflictError, [ {}, conflictError ] ],
+ partiallySucceeded: []
+ }
+ }, {
+ data: {},
+ expected: {
+ succeeded: [],
+ failed: [ {} ],
+ partiallySucceeded: []
+ }
+ }, {
+ data: testNode1,
+ expected: {
+ succeeded: [ testNode1 ],
+ failed: [],
+ partiallySucceeded: []
+ }
+ }, {
+ data: {itemMoved: testNode1, initialParentId: parentID},
+ expected: {
+ succeeded: [ {itemMoved: testNode1, initialParentId: parentID} ],
+ failed: [],
+ partiallySucceeded: []
+ }
+ }
+ ];
+
+ testData.forEach((response) => {
+ it(`processed response should be \'${response.expected}\' for given input: \'${response.data}\'`, () => {
+ const result = service.processResponse(response.data);
+
+ expect(JSON.stringify(result)).toEqual(JSON.stringify(response.expected));
+ });
+
+ });
+ });
+
+});
diff --git a/src/app/common/services/node-actions.service.ts b/src/app/common/services/node-actions.service.ts
new file mode 100644
index 000000000..46d77fec5
--- /dev/null
+++ b/src/app/common/services/node-actions.service.ts
@@ -0,0 +1,580 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Observable, Subject } from 'rxjs/Rx';
+
+import { AlfrescoApiService, AlfrescoContentService, NodesApiService } from 'ng2-alfresco-core';
+import { DataColumn } from 'ng2-alfresco-datatable';
+import { DocumentListService, ContentNodeSelectorComponent, ContentNodeSelectorComponentData, ShareDataRow } from 'ng2-alfresco-documentlist';
+import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
+
+@Injectable()
+export class NodeActionsService {
+ static SNACK_MESSAGE_DURATION_WITH_UNDO: number = 10000;
+ static SNACK_MESSAGE_DURATION: number = 3000;
+
+ contentCopied: Subject = new Subject();
+ contentMoved: Subject = new Subject();
+ moveDeletedEntries: any[] = [];
+
+ constructor(private contentService: AlfrescoContentService,
+ private dialog: MdDialog,
+ private documentListService: DocumentListService,
+ private apiService: AlfrescoApiService,
+ private nodesApi: NodesApiService) {}
+
+ /**
+ * Copy node list
+ *
+ * @param contentEntities nodes to copy
+ * @param permission permission which is needed to apply the action
+ */
+ public copyNodes(contentEntities: any[], permission?: string): Subject {
+ return this.doBatchOperation('copy', contentEntities, permission);
+ }
+
+ /**
+ * Move node list
+ *
+ * @param contentEntities nodes to move
+ * @param permission permission which is needed to apply the action
+ */
+ public moveNodes(contentEntities: any[], permission?: string): Subject {
+ return this.doBatchOperation('move', contentEntities, permission);
+ }
+
+ /**
+ * General method for performing the given operation (copy|move) to multiple nodes
+ *
+ * @param action the action to perform (copy|move)
+ * @param contentEntities the contentEntities which have to have the action performed on
+ * @param permission permission which is needed to apply the action
+ */
+ private doBatchOperation(action: string, contentEntities: any[], permission?: string): Subject {
+ const observable: Subject = new Subject();
+
+ if (!this.isEntryEntitiesArray(contentEntities)) {
+ observable.error(new Error(JSON.stringify({error: {statusCode: 400}})));
+
+ } else if (this.checkPermission(action, contentEntities, permission)) {
+
+ const destinationSelection = this.getContentNodeSelection(action, contentEntities);
+ destinationSelection.subscribe((selections: MinimalNodeEntryEntity[]) => {
+
+ const contentEntry = contentEntities[0].entry ;
+ // Check if there's nodeId for Shared Files
+ const contentEntryId = contentEntry.nodeId || contentEntry.id;
+ const type = contentEntry.isFolder ? 'folder' : 'content';
+ const batch = [];
+
+ // consider only first item in the selection
+ const selection = selections[0];
+ let action$: Observable;
+
+ if (action === 'move' && contentEntities.length === 1 && type === 'content') {
+ action$ = this.documentListService[`${action}Node`].call(this.documentListService, contentEntryId, selection.id);
+ action$ = action$.toArray();
+
+ } else {
+ contentEntities.forEach((node) => {
+ batch.push(this[`${action}NodeAction`](node.entry, selection.id));
+ });
+ action$ = Observable.zip(...batch);
+ }
+
+ action$
+ .subscribe(
+ (newContent) => {
+ observable.next(`OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`);
+
+ if (action === 'copy') {
+ this.contentCopied.next(newContent);
+
+ } else if (action === 'move') {
+ const processedData = this.processResponse(newContent);
+ this.contentMoved.next(processedData);
+ }
+
+ },
+ observable.error.bind(observable)
+ );
+ this.dialog.closeAll();
+ });
+
+ } else {
+ observable.error(new Error(JSON.stringify({error: {statusCode: 403}})));
+ }
+
+ return observable;
+ }
+
+ isEntryEntitiesArray(contentEntities: any[]): boolean {
+ if (contentEntities && contentEntities.length) {
+ const nonEntryNode = contentEntities.find(node => (!node || !node.entry || !(node.entry.nodeId || node.entry.id)));
+ return !nonEntryNode;
+ }
+ return false;
+ }
+
+ checkPermission(action: string, contentEntities: any[], permission?: string) {
+ const notAllowedNode = contentEntities.find(node => !this.isActionAllowed(action, node.entry, permission));
+ return !notAllowedNode;
+ }
+
+ getFirstParentId(nodeEntities: any[]): string {
+ for (let i = 0; i < nodeEntities.length; i++) {
+ const nodeEntry = nodeEntities[i].entry;
+
+ if (nodeEntry.parentId) {
+ return nodeEntry.parentId;
+
+ } else if (nodeEntry.path && nodeEntry.path.elements && nodeEntry.path.elements.length) {
+ return nodeEntry.path.elements[nodeEntry.path.elements.length - 1].id;
+ }
+ }
+
+ // if no parent data is found, return the id of first item / the nodeId in case of Shared Files
+ return nodeEntities[0].entry.nodeId || nodeEntities[0].entry.id;
+ }
+
+ getEntryParentId(nodeEntry: any) {
+ let entryParentId = '';
+
+ if (nodeEntry.parentId) {
+ entryParentId = nodeEntry.parentId;
+
+ } else if (nodeEntry.path && nodeEntry.path.elements && nodeEntry.path.elements.length) {
+ entryParentId = nodeEntry.path.elements[nodeEntry.path.elements.length - 1].id;
+ }
+
+ return entryParentId;
+ }
+
+ getContentNodeSelection(action: string, contentEntities: MinimalNodeEntity[]): EventEmitter {
+ const currentParentFolderId = this.getFirstParentId(contentEntities);
+
+ let nodeEntryName = '';
+ if (contentEntities.length === 1 && contentEntities[0].entry.name) {
+ nodeEntryName = `${contentEntities[0].entry.name} `;
+ }
+
+ const data: ContentNodeSelectorComponentData = {
+ title: `${action} ${nodeEntryName}to ...`,
+ currentFolderId: currentParentFolderId,
+ rowFilter: this.rowFilter.bind(this),
+ imageResolver: this.imageResolver.bind(this),
+ select: new EventEmitter()
+ };
+
+ this.dialog.open(ContentNodeSelectorComponent, {
+ data,
+ panelClass: 'adf-content-node-selector-dialog',
+ width: '630px'
+ });
+
+ return data.select;
+ }
+
+ copyNodeAction(nodeEntry, selectionId): Observable {
+ if (nodeEntry.isFolder) {
+ return this.copyFolderAction(nodeEntry, selectionId);
+
+ } else {
+ // any other type is treated as 'content'
+ return this.copyContentAction(nodeEntry, selectionId);
+ }
+ }
+
+ copyContentAction(contentEntry, selectionId, oldName?): Observable {
+ const _oldName = oldName || contentEntry.name;
+ // Check if there's nodeId for Shared Files
+ const contentEntryId = contentEntry.nodeId || contentEntry.id;
+
+ // use local method until new name parameter is added on ADF copyNode
+ return this.copyNode(contentEntryId, selectionId, _oldName)
+ .catch((err) => {
+ let errStatusCode;
+ try {
+ const {error: {statusCode}} = JSON.parse(err.message);
+ errStatusCode = statusCode;
+ } catch (e) { //
+ }
+
+ if (errStatusCode && errStatusCode === 409) {
+ return this.copyContentAction(contentEntry, selectionId, this.getNewNameFrom(_oldName, contentEntry.name));
+ } else {
+ return Observable.throw(err || 'Server error');
+ }
+ });
+ }
+
+ copyFolderAction(contentEntry, selectionId): Observable {
+ // Check if there's nodeId for Shared Files
+ const contentEntryId = contentEntry.nodeId || contentEntry.id;
+ let $destinationFolder: Observable;
+ let $childrenToCopy: Observable;
+ let newDestinationFolder;
+
+ return this.copyNode(contentEntryId, selectionId, contentEntry.name)
+ .catch((err) => {
+ let errStatusCode;
+ try {
+ const {error: {statusCode}} = JSON.parse(err.message);
+ errStatusCode = statusCode;
+ } catch (e) { //
+ }
+
+ if (errStatusCode && errStatusCode === 409) {
+
+ $destinationFolder = this.getChildByName(selectionId, contentEntry.name);
+ $childrenToCopy = this.getNodeChildren(contentEntryId);
+
+ return $destinationFolder
+ .flatMap((destination) => {
+ newDestinationFolder = destination;
+ return $childrenToCopy;
+ })
+ .flatMap((nodesToCopy) => {
+ const batch = [];
+ nodesToCopy.list.entries.forEach((node) => {
+ if (node.entry.isFolder) {
+ batch.push(this.copyFolderAction(node.entry, newDestinationFolder.entry.id));
+
+ } else {
+ batch.push(this.copyContentAction(node.entry, newDestinationFolder.entry.id));
+ }
+ });
+
+ if (!batch.length) {
+ return Observable.of({});
+ }
+ return Observable.zip(...batch);
+ });
+
+ } else {
+ return Observable.throw(err || 'Server error');
+ }
+ });
+ }
+
+ moveNodeAction(nodeEntry, selectionId): Observable {
+ this.moveDeletedEntries = [];
+
+ if (nodeEntry.isFolder) {
+ const initialParentId = nodeEntry.parentId;
+
+ return this.moveFolderAction(nodeEntry, selectionId)
+ .flatMap((newContent) => {
+
+ // take no extra action, if folder is moved to the same location
+ if (initialParentId === selectionId) {
+ return Observable.of(newContent);
+ }
+
+ const flattenResponse = this.flatten(newContent);
+ const processedData = this.processResponse(flattenResponse);
+
+ // else, check if moving this nodeEntry succeeded for ALL of its nodes
+ if (processedData.failed.length === 0) {
+
+ // check if folder still exists on location
+ return this.getChildByName(initialParentId, nodeEntry.name)
+ .flatMap((folderOnInitialLocation) => {
+
+ if (folderOnInitialLocation) {
+ // Check if there's nodeId for Shared Files
+ const nodeEntryId = nodeEntry.nodeId || nodeEntry.id;
+ // delete it from location
+ return this.nodesApi.deleteNode(nodeEntryId)
+ .flatMap(() => {
+ this.moveDeletedEntries.push(nodeEntry);
+ return Observable.of(newContent);
+ });
+ }
+ return Observable.of(newContent);
+ });
+
+ }
+ return Observable.of(newContent);
+ });
+
+ } else {
+ // any other type is treated as 'content'
+ return this.moveContentAction(nodeEntry, selectionId);
+ }
+ }
+
+ moveFolderAction(contentEntry, selectionId): Observable {
+ // Check if there's nodeId for Shared Files
+ const contentEntryId = contentEntry.nodeId || contentEntry.id;
+ const initialParentId = this.getEntryParentId(contentEntry);
+ let $destinationFolder: Observable;
+ let $childrenToMove: Observable;
+ let newDestinationFolder;
+
+ return this.documentListService.moveNode(contentEntryId, selectionId)
+ .map((itemMoved) => {
+ return { itemMoved, initialParentId };
+ })
+ .catch((err) => {
+ let errStatusCode;
+ try {
+ const {error: {statusCode}} = JSON.parse(err.message);
+ errStatusCode = statusCode;
+ } catch (e) { //
+ }
+
+ if (errStatusCode && errStatusCode === 409) {
+
+ $destinationFolder = this.getChildByName(selectionId, contentEntry.name);
+ $childrenToMove = this.getNodeChildren(contentEntryId);
+
+ return $destinationFolder
+ .flatMap((destination) => {
+ newDestinationFolder = destination;
+ return $childrenToMove;
+ })
+ .flatMap((childrenToMove) => {
+ const batch = [];
+ childrenToMove.list.entries.forEach((node) => {
+ if (node.entry.isFolder) {
+ batch.push(this.moveFolderAction(node.entry, newDestinationFolder.entry.id));
+
+ } else {
+ batch.push(this.moveContentAction(node.entry, newDestinationFolder.entry.id));
+ }
+ });
+
+ if (!batch.length) {
+ return Observable.of(batch);
+ }
+ return Observable.zip(...batch);
+ });
+ } else {
+ return Observable.throw(err);
+ }
+ });
+ }
+
+ moveContentAction(contentEntry, selectionId) {
+ // Check if there's nodeId for Shared Files
+ const contentEntryId = contentEntry.nodeId || contentEntry.id;
+ const initialParentId = this.getEntryParentId(contentEntry);
+
+ return this.documentListService.moveNode(contentEntryId, selectionId)
+ .map((itemMoved) => {
+ return { itemMoved, initialParentId };
+ })
+ .catch((err) => {
+ let errStatusCode;
+ try {
+ const {error: {statusCode}} = JSON.parse(err.message);
+ errStatusCode = statusCode;
+ } catch (e) { //
+ }
+
+ if (errStatusCode && errStatusCode === 409) {
+ // do not throw error, to be able to show message in case of partial move of files
+ return Observable.of(err);
+ } else {
+ return Observable.throw(err);
+ }
+ });
+ }
+
+ getChildByName(parentId, name) {
+ let matchedNodes: Subject = new Subject();
+
+ this.getNodeChildren(parentId).subscribe(
+ (childrenNodes) => {
+ const result = childrenNodes.list.entries.find(node => (node.entry.name === name));
+
+ if (result) {
+ matchedNodes.next(result);
+
+ } else {
+ matchedNodes.next(null);
+ }
+ },
+ (err) => {
+ return Observable.throw(err || 'Server error');
+ });
+ return matchedNodes;
+ }
+
+ private isActionAllowed(action: string, node: MinimalNodeEntryEntity, permission?: string): boolean {
+ if (action === 'copy') {
+ return true;
+ }
+ return this.contentService.hasPermission(node, permission);
+ }
+
+ private rowFilter(row: ShareDataRow): boolean {
+ const node: MinimalNodeEntryEntity = row.node.entry;
+ return (!node.isFile);
+ }
+
+ 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;
+ }
+
+ public getNewNameFrom(name: string, baseName?: string) {
+ const extensionMatch = name.match(/\.[^/.]+$/);
+
+ // remove extension in case there is one
+ let fileExtension = extensionMatch ? extensionMatch[0] : '';
+ let extensionFree = extensionMatch ? name.slice(0, extensionMatch.index) : name;
+
+ let prefixNumber = 1;
+ let baseExtensionFree;
+
+ if (baseName) {
+ const baseExtensionMatch = baseName.match(/\.[^/.]+$/);
+
+ // remove extension in case there is one
+ baseExtensionFree = baseExtensionMatch ? baseName.slice(0, baseExtensionMatch.index) : baseName;
+ }
+
+ if (!baseExtensionFree || baseExtensionFree !== extensionFree) {
+
+ // check if name already has integer appended on end:
+ const oldPrefix = extensionFree.match('-[0-9]+$');
+ if (oldPrefix) {
+
+ // if so, try to get the number at the end
+ const oldPrefixNumber = parseInt(oldPrefix[0].slice(1), 10);
+ if (oldPrefixNumber.toString() === oldPrefix[0].slice(1)) {
+
+ extensionFree = extensionFree.slice(0, oldPrefix.index);
+ prefixNumber = oldPrefixNumber + 1;
+ }
+
+ }
+
+ }
+ return extensionFree + '-' + prefixNumber + fileExtension;
+ }
+
+ /**
+ * Get children nodes of given parent node
+ *
+ * @param nodeId The id of the parent node
+ * @param params optional parameters
+ */
+ getNodeChildren(nodeId: string, params?) {
+ return Observable.fromPromise(this.apiService.getInstance().nodes.getNodeChildren(nodeId, params));
+ }
+
+ // Copied from ADF document-list.service, and added the name parameter
+ /**
+ * 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
+ * @param name The new name for the copy that would be added on the destination folder
+ */
+ copyNode(nodeId: string, targetParentId: string, name?: string) {
+ return Observable.fromPromise(this.apiService.getInstance().nodes.copyNode(nodeId, {targetParentId, name}));
+ }
+
+ public flatten(nDimArray) {
+ if (!Array.isArray(nDimArray)) {
+ return nDimArray;
+ }
+
+ let nodeQueue = nDimArray.slice(0);
+ let resultingArray = [];
+
+ do {
+ nodeQueue.forEach(
+ (node) => {
+ if (Array.isArray(node)) {
+ nodeQueue.push(...node);
+ } else {
+ resultingArray.push(node);
+ }
+
+ const nodeIndex = nodeQueue.indexOf(node);
+ nodeQueue.splice(nodeIndex, 1);
+ }
+ );
+ } while (nodeQueue.length);
+
+ return resultingArray;
+ }
+
+ processResponse(data: any): any {
+ const moveStatus = {
+ succeeded: [],
+ failed: [],
+ partiallySucceeded: []
+ };
+
+ if (Array.isArray(data)) {
+ return data.reduce(
+ (acc, next) => {
+
+ if (next instanceof Error) {
+ acc.failed.push(next);
+
+ } else if (Array.isArray(next)) {
+ // if content of a folder was moved
+
+ const folderMoveResponseData = this.flatten(next);
+ const foundError = folderMoveResponseData.find(node => node instanceof Error);
+ // data might contain also items of form: { itemMoved, initialParentId }
+ const foundEntry = folderMoveResponseData.find(node => (node.itemMoved && node.itemMoved.entry) || (node && node.entry));
+
+ if (!foundError) {
+ // consider success if NONE of the items from the folder move response is an error
+ acc.succeeded.push(next);
+
+ } else if (!foundEntry) {
+ // consider failed if NONE of the items has an entry
+ acc.failed.push(next);
+
+ } else {
+ // partially move folder
+ acc.partiallySucceeded.push(next);
+ }
+
+ } else {
+ acc.succeeded.push(next);
+ }
+
+ return acc;
+ },
+ moveStatus
+ );
+ } else {
+ if ((data.itemMoved && data.itemMoved.entry) || (data && data.entry)) {
+ moveStatus.succeeded.push(data);
+ } else {
+ moveStatus.failed.push(data);
+ }
+
+ return moveStatus;
+ }
+ }
+}
diff --git a/src/app/components/current-user/current-user.component.html b/src/app/components/current-user/current-user.component.html
new file mode 100644
index 000000000..852185f86
--- /dev/null
+++ b/src/app/components/current-user/current-user.component.html
@@ -0,0 +1,18 @@
+
+
+
+ {{ userName }}
+
+ {{ userInitials }}
+
+
+
+
+
+
+
+
diff --git a/src/app/components/current-user/current-user.component.scss b/src/app/components/current-user/current-user.component.scss
new file mode 100644
index 000000000..18a48aea1
--- /dev/null
+++ b/src/app/components/current-user/current-user.component.scss
@@ -0,0 +1,36 @@
+@import './../../ui/variables';
+
+$am-avatar-size: 40px;
+
+$am-avatar-light-bg: rgba(white, .15);
+$am-avatar-dark-bg: rgba(black, .15);
+
+:host {
+ font-weight: lighter;
+ position: relative;
+
+ color: $alfresco-white;
+ line-height: 20px;
+
+ .am-avatar {
+ margin-left: 5px;
+ cursor: pointer;
+
+ display: inline-block;
+ width: $am-avatar-size;
+ height: $am-avatar-size;
+ line-height: $am-avatar-size;
+ font-size: 1.2em;
+ text-align: center;
+ color: inherit;
+ border-radius: 100%;
+
+ &--light {
+ background: $am-avatar-light-bg;
+ }
+
+ &--dark {
+ background: $am-avatar-dark-bg;
+ }
+ }
+}
diff --git a/src/app/components/current-user/current-user.component.spec.ts b/src/app/components/current-user/current-user.component.spec.ts
new file mode 100644
index 000000000..7ca5239a4
--- /dev/null
+++ b/src/app/components/current-user/current-user.component.spec.ts
@@ -0,0 +1,66 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, async } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Observable } from 'rxjs/Rx';
+import { MaterialModule } from '@angular/material';
+import { CoreModule, PeopleContentService } from 'ng2-alfresco-core';
+
+import { CurrentUserComponent } from './current-user.component';
+
+describe('CurrentUserComponent', () => {
+ let fixture;
+ let component;
+ let peopleApi: PeopleContentService;
+ let user;
+
+ beforeEach(() => {
+ user = { entry: { firstName: 'joe', lastName: 'doe' } };
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ MaterialModule,
+ CoreModule,
+ RouterTestingModule
+ ],
+ declarations: [
+ CurrentUserComponent
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(CurrentUserComponent);
+ component = fixture.componentInstance;
+ peopleApi = TestBed.get(PeopleContentService);
+
+ spyOn(peopleApi, 'getCurrentPerson').and.returnValue(Observable.of(user));
+
+ fixture.detectChanges();
+ });
+ }));
+
+ it('updates user data', () => {
+ expect(component.user).toBe(user.entry);
+ });
+
+ it('get user initials', () => {
+ expect(component.userInitials).toBe('jd');
+ });
+});
diff --git a/src/app/components/current-user/current-user.component.ts b/src/app/components/current-user/current-user.component.ts
new file mode 100644
index 000000000..39904320e
--- /dev/null
+++ b/src/app/components/current-user/current-user.component.ts
@@ -0,0 +1,63 @@
+/*!
+ * @license
+ * Copyright 2017 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, OnInit, OnDestroy } from '@angular/core';
+import { PeopleContentService } from 'ng2-alfresco-core';
+import { Subscription } from 'rxjs/Rx';
+
+@Component({
+ selector: 'app-current-user',
+ templateUrl: './current-user.component.html',
+ styleUrls: [ './current-user.component.scss' ]
+})
+export class CurrentUserComponent implements OnInit, OnDestroy {
+ private user: any = null;
+ private personSubscription: Subscription;
+
+ constructor(private peopleApi: PeopleContentService) {}
+
+ ngOnInit() {
+ this.personSubscription = this.peopleApi.getCurrentPerson()
+ .subscribe((person: any) => {
+ this.user = person.entry;
+ });
+ }
+
+ ngOnDestroy() {
+ this.personSubscription.unsubscribe();
+ }
+
+ get userFirstName(): string {
+ const { user } = this;
+ return user ? (user.firstName || '') : '';
+ }
+
+ get userLastName(): string {
+ const { user } = this;
+ return user ? (user.lastName || '') : '';
+ }
+
+ get userName(): string {
+ const { userFirstName: first, userLastName: last } = this;
+ return `${first} ${last}`;
+ }
+
+ get userInitials(): string {
+ const { userFirstName: first, userLastName: last } = this;
+ return [ first[0], last[0] ].join('');
+ }
+}
diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html
new file mode 100644
index 000000000..bfa1a8880
--- /dev/null
+++ b/src/app/components/favorites/favorites.component.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/favorites/favorites.component.spec.ts b/src/app/components/favorites/favorites.component.spec.ts
new file mode 100644
index 000000000..0728982b9
--- /dev/null
+++ b/src/app/components/favorites/favorites.component.spec.ts
@@ -0,0 +1,189 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, async } from '@angular/core/testing';
+import { Observable } from 'rxjs/Rx';
+
+import { CoreModule, NodesApiService, AlfrescoApiService } from 'ng2-alfresco-core';
+import { CommonModule } from '../../common/common.module';
+
+import { ContentManagementService } from '../../common/services/content-management.service';
+
+import { FavoritesComponent } from './favorites.component';
+
+describe('Favorites Routed Component', () => {
+ let fixture;
+ let component: FavoritesComponent;
+ let nodesApi: NodesApiService;
+ let alfrescoApi: AlfrescoApiService;
+ let contentService: ContentManagementService;
+ let router: Router;
+ let page;
+ let node;
+
+ beforeAll(() => {
+ // testing only functional-wise not time-wise
+ Observable.prototype.debounceTime = function () { return this; };
+ });
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: [
+ { entry: { id: 1, target: { file: {} } } },
+ { entry: { id: 2, target: { folder: {} } } }
+ ],
+ pagination: { data: 'data'}
+ }
+ };
+
+ node = {
+ id: 'folder-node',
+ isFolder: true,
+ isFile: false,
+ path: {
+ elements: []
+ }
+ };
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ CommonModule,
+ RouterTestingModule
+ ],
+ declarations: [
+ FavoritesComponent
+ ]
+ })
+ .compileComponents().then(() => {
+ fixture = TestBed.createComponent(FavoritesComponent);
+ component = fixture.componentInstance;
+
+ nodesApi = TestBed.get(NodesApiService);
+ alfrescoApi = TestBed.get(AlfrescoApiService);
+ contentService = TestBed.get(ContentManagementService);
+ router = TestBed.get(Router);
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(alfrescoApi.favoritesApi, 'getFavorites').and.returnValue(Promise.resolve(page));
+ });
+
+ describe('Events', () => {
+ it('should refresh on editing folder event', () => {
+ spyOn(component, 'refresh');
+ fixture.detectChanges();
+
+ contentService.editFolder.next(null);
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should refresh on move node event', () => {
+ spyOn(component, 'refresh');
+ fixture.detectChanges();
+
+ contentService.moveNode.next(null);
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should fetch nodes on favorite toggle', () => {
+ spyOn(component, 'refresh');
+ fixture.detectChanges();
+
+ contentService.toggleFavorite.next(null);
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+ });
+
+ describe('Node navigation', () => {
+ beforeEach(() => {
+ spyOn(nodesApi, 'getNode').and.returnValue(Observable.of(node));
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ });
+
+ it('navigates to `/libraries` if node path has `Sites`', () => {
+ node.path.elements = [{ name: 'Sites' }];
+
+ component.navigate(node);
+
+ expect(router.navigate).toHaveBeenCalledWith([ '/libraries', 'folder-node' ]);
+ });
+
+ it('navigates to `/personal-files` if node path has no `Sites`', () => {
+ node.path.elements = [{ name: 'something else' }];
+
+ component.navigate(node);
+
+ expect(router.navigate).toHaveBeenCalledWith([ '/personal-files', 'folder-node' ]);
+ });
+
+ it('does not navigate when node is not folder', () => {
+ node.isFolder = false;
+
+ component.navigate(node);
+
+ expect(router.navigate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('onNodeDoubleClick', () => {
+ beforeEach(() => {
+ spyOn(nodesApi, 'getNode').and.returnValue(Observable.of(node));
+ fixture.detectChanges();
+ });
+
+ it('navigates if node is a folder', () => {
+ node.isFolder = true;
+ spyOn(router, 'navigate');
+
+ component.onNodeDoubleClick(node);
+
+ expect(router.navigate).toHaveBeenCalled();
+ });
+
+ it('opens preview if node is a file', () => {
+ node.isFolder = false;
+ node.isFile = true;
+ spyOn(router, 'navigate').and.stub();
+
+ component.onNodeDoubleClick(node);
+
+ expect(router.navigate).toHaveBeenCalledWith(['/preview', node.id]);
+ });
+ });
+
+ describe('refresh', () => {
+ it('should call document list reload', () => {
+ spyOn(component.documentList, 'reload');
+ fixture.detectChanges();
+
+ component.refresh();
+
+ expect(component.documentList.reload).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts
new file mode 100644
index 000000000..7cda3897d
--- /dev/null
+++ b/src/app/components/favorites/favorites.component.ts
@@ -0,0 +1,100 @@
+/*!
+ * @license
+ * Copyright 2017 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, ViewChild, OnInit, OnDestroy } from '@angular/core';
+import { Router } from '@angular/router';
+import { Subscription } from 'rxjs/Rx';
+
+import { MinimalNodeEntryEntity, PathElementEntity, PathInfoEntity } from 'alfresco-js-api';
+import { NodesApiService } from 'ng2-alfresco-core';
+import { DocumentListComponent } from 'ng2-alfresco-documentlist';
+
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { PageComponent } from '../page.component';
+
+@Component({
+ templateUrl: './favorites.component.html'
+})
+export class FavoritesComponent extends PageComponent implements OnInit, OnDestroy {
+
+ @ViewChild(DocumentListComponent)
+ documentList: DocumentListComponent;
+
+ private onEditFolder: Subscription;
+ private onMoveNode: Subscription;
+ private onToggleFavorite: Subscription;
+
+ constructor(
+ private router: Router,
+ private nodesApi: NodesApiService,
+ private content: ContentManagementService) {
+ super();
+ }
+
+ ngOnInit() {
+ this.onEditFolder = this.content.editFolder.subscribe(() => this.refresh());
+ this.onMoveNode = this.content.moveNode.subscribe(() => this.refresh());
+ this.onToggleFavorite = this.content.toggleFavorite
+ .debounceTime(300).subscribe(() => this.refresh());
+ }
+
+ ngOnDestroy() {
+ this.onEditFolder.unsubscribe();
+ this.onMoveNode.unsubscribe();
+ this.onToggleFavorite.unsubscribe();
+ }
+
+ fetchNodes(): void {
+ // todo: remove once all views migrate to native data source
+ }
+
+ navigate(favorite: MinimalNodeEntryEntity) {
+ const { isFolder, id } = favorite;
+
+ // TODO: rework as it will fail on non-English setups
+ const isSitePath = (path: PathInfoEntity): boolean => {
+ return path.elements.some(({ name }: PathElementEntity) => (name === 'Sites'));
+ };
+
+ if (isFolder) {
+ this.nodesApi
+ .getNode(id)
+ .subscribe(({ path }: MinimalNodeEntryEntity) => {
+ const routeUrl = isSitePath(path) ? '/libraries' : '/personal-files';
+ this.router.navigate([ routeUrl, id ]);
+ });
+ }
+ }
+
+ onNodeDoubleClick(node: MinimalNodeEntryEntity) {
+ if (node) {
+ if (node.isFolder) {
+ this.navigate(node);
+ }
+
+ if (node.isFile) {
+ this.router.navigate(['/preview', node.id]);
+ }
+ }
+ }
+
+ refresh(): void {
+ if (this.documentList) {
+ this.documentList.reload();
+ }
+ }
+}
diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html
new file mode 100644
index 000000000..fcaafd62e
--- /dev/null
+++ b/src/app/components/files/files.component.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+ {{ value | adfFileSize }}
+
+
+
+
+
+ {{ value | adfTimeAgo }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts
new file mode 100644
index 000000000..e4ad406fe
--- /dev/null
+++ b/src/app/components/files/files.component.spec.ts
@@ -0,0 +1,427 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Observable } from 'rxjs/Rx';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, async } from '@angular/core/testing';
+import { UploadService, NodesApiService, FileUploadCompleteEvent,
+ FileUploadDeleteEvent, FileModel, AlfrescoContentService } from 'ng2-alfresco-core';
+
+import { CommonModule } from '../../common/common.module';
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+import { NodeActionsService } from '../../common/services/node-actions.service';
+import { FilesComponent } from './files.component';
+
+describe('FilesComponent', () => {
+ let node;
+ let page;
+ let fixture;
+ let component: FilesComponent;
+ let contentManagementService: ContentManagementService;
+ let alfrescoContentService: AlfrescoContentService;
+ let uploadService: UploadService;
+ let nodesApi: NodesApiService;
+ let router: Router;
+ let browsingFilesService: BrowsingFilesService;
+ let nodeActionsService: NodeActionsService;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ FilesComponent
+ ]
+ }).compileComponents()
+ .then(() => {
+
+ fixture = TestBed.createComponent(FilesComponent);
+ component = fixture.componentInstance;
+
+ contentManagementService = TestBed.get(ContentManagementService);
+ uploadService = TestBed.get(UploadService);
+ nodesApi = TestBed.get(NodesApiService);
+ router = TestBed.get(Router);
+ alfrescoContentService = TestBed.get(AlfrescoContentService);
+ browsingFilesService = TestBed.get(BrowsingFilesService);
+ nodeActionsService = TestBed.get(NodeActionsService);
+ });
+ }));
+
+ beforeEach(() => {
+ node = { id: 'node-id' };
+ page = {
+ list: {
+ entries: ['a', 'b', 'c'],
+ pagination: {}
+ }
+ };
+ });
+
+ describe('OnInit', () => {
+ it('set current node', () => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+
+ expect(component.node).toBe(node);
+ });
+
+ it('get current node children', () => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+
+ expect(component.paging).toBe(page);
+ });
+
+ it('emits onChangeParent event', () => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+ spyOn(browsingFilesService.onChangeParent, 'next').and.callFake((val) => val);
+
+ fixture.detectChanges();
+
+ expect(browsingFilesService.onChangeParent.next)
+ .toHaveBeenCalledWith(node);
+ });
+
+ it('raise error when fetchNode() fails', () => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.throw(null));
+ spyOn(component, 'onFetchError');
+
+ fixture.detectChanges();
+
+ expect(component.onFetchError).toHaveBeenCalled();
+ });
+
+ it('raise error when fetchNodes() fails', () => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.throw(null));
+ spyOn(component, 'onFetchError');
+
+ fixture.detectChanges();
+
+ expect(component.onFetchError).toHaveBeenCalled();
+ });
+ });
+
+ describe('refresh on events', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+ spyOn(component, 'load');
+
+ fixture.detectChanges();
+ });
+
+ it('reset favorites colection onToggleFavorite event', () => {
+ contentManagementService.toggleFavorite.next(null);
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh onContentCopied event if parent is the same', () => {
+ const nodes = [
+ { entry: { parentId: '1' } },
+ { entry: { parentId: '2' } }
+ ];
+
+ component.node = { id: '1' };
+
+ nodeActionsService.contentCopied.next(nodes);
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('does not call refresh onContentCopied event when parent mismatch', () => {
+ const nodes = [
+ { entry: { parentId: '1' } },
+ { entry: { parentId: '2' } }
+ ];
+
+ component.node = { id: '3' };
+
+ nodeActionsService.contentCopied.next(nodes);
+
+ expect(component.load).not.toHaveBeenCalled();
+ });
+
+ it('calls refresh onCreateFolder event', () => {
+ contentManagementService.createFolder.next();
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh editFolder event', () => {
+ contentManagementService.editFolder.next();
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh deleteNode event', () => {
+ contentManagementService.deleteNode.next();
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh moveNode event', () => {
+ contentManagementService.moveNode.next();
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh restoreNode event', () => {
+ contentManagementService.restoreNode.next();
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('calls refresh on fileUploadComplete event if parent node match', () => {
+ const file = { file: { options: { parentId: 'parentId' } } };
+ component.node = { id: 'parentId' };
+
+ uploadService.fileUploadComplete.next(file);
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('does not call refresh on fileUploadComplete event if parent mismatch', () => {
+ const file = { file: { options: { parentId: 'otherId' } } };
+ component.node = { id: 'parentId' };
+
+ uploadService.fileUploadComplete.next(file);
+
+ expect(component.load).not.toHaveBeenCalled();
+ });
+
+ it('calls refresh on fileUploadDeleted event if parent node match', () => {
+ const file = { file: { options: { parentId: 'parentId' } } };
+ component.node = { id: 'parentId' };
+
+ uploadService.fileUploadDeleted.next(file);
+
+ expect(component.load).toHaveBeenCalled();
+ });
+
+ it('does not call refresh on fileUploadDeleted event if parent mismatch', () => {
+ const file = { file: { options: { parentId: 'otherId' } } };
+ component.node = { id: 'parentId' };
+
+ uploadService.fileUploadDeleted.next(file);
+
+ expect(component.load).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchNode()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+ spyOn(nodesApi, 'getNode').and.returnValue(Observable.of(node));
+
+ fixture.detectChanges();
+ });
+
+ it('calls getNode api with node id', () => {
+ component.fetchNode('nodeId');
+
+ expect(nodesApi.getNode).toHaveBeenCalledWith('nodeId');
+ });
+ });
+
+ describe('fetchNodes()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(nodesApi, 'getNodeChildren').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+ });
+
+ it('calls getNode api with node id', () => {
+ component.fetchNodes('nodeId');
+
+ expect(nodesApi.getNodeChildren).toHaveBeenCalledWith('nodeId', {});
+ });
+ });
+
+ describe('Create permission', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+ });
+
+ it('returns false when node is not provided', () => {
+ expect(component.canCreateContent(null)).toBe(false);
+ });
+
+ it('returns false when node does not have permission', () => {
+ spyOn(alfrescoContentService, 'hasPermission').and.returnValue(false);
+
+ expect(component.canCreateContent(node)).toBe(false);
+ });
+
+ it('returns false when node has permission', () => {
+ spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
+
+ expect(component.canCreateContent(node)).toBe(true);
+ });
+ });
+
+ describe('onNodeDoubleClick()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+ });
+
+ it('opens preview if node is file', () => {
+ spyOn(router, 'navigate').and.stub();
+ node.isFile = true;
+
+ component.onNodeDoubleClick( node);
+
+ expect(router.navigate).toHaveBeenCalledWith(['/preview', node.id]);
+ });
+
+ it('navigate if node is folder', () => {
+ spyOn(component, 'navigate').and.stub();
+ node.isFolder = true;
+
+ component.onNodeDoubleClick( node);
+
+ expect(component.navigate).toHaveBeenCalled();
+ });
+ });
+
+ describe('load()', () => {
+ let fetchNodesSpy;
+
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ fetchNodesSpy = spyOn(component, 'fetchNodes');
+
+ fetchNodesSpy.and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ fetchNodesSpy.calls.reset();
+ });
+
+ it('shows load indicator', () => {
+ spyOn(component, 'onPageLoaded');
+ component.node = { id: 'currentNode' };
+
+ expect(component.isLoading).toBe(false);
+
+ component.load(true);
+
+ expect(component.isLoading).toBe(true);
+ });
+
+ it('does not show load indicator', () => {
+ spyOn(component, 'onPageLoaded');
+ component.node = { id: 'currentNode' };
+
+ expect(component.isLoading).toBe(false);
+
+ component.load();
+
+ expect(component.isLoading).toBe(false);
+ });
+
+ it('sets data on success', () => {
+ component.node = { id: 'currentNode' };
+
+ component.load();
+
+ expect(component.paging).toBe(page);
+ expect(component.pagination).toBe(page.list.pagination);
+ });
+
+ it('raise error on fail', () => {
+ fetchNodesSpy.and.returnValue(Observable.throw(null));
+ spyOn(component, 'onFetchError');
+
+ component.load();
+
+ expect(component.onFetchError).toHaveBeenCalled();
+ });
+ });
+
+ describe('onBreadcrumbNavigate()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+
+ fixture.detectChanges();
+ });
+
+ it('navigates to node id', () => {
+ const routeData = { id: 'some-where-over-the-rainbow' };
+ spyOn(component, 'navigate');
+
+ component.onBreadcrumbNavigate(routeData);
+
+ expect(component.navigate).toHaveBeenCalledWith(routeData.id);
+ });
+ });
+
+ describe('Node navigation', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
+ spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
+ spyOn(router, 'navigate');
+
+ fixture.detectChanges();
+ });
+
+ it('navigates to node when id provided', () => {
+ component.navigate(node.id);
+
+ expect(router.navigate).toHaveBeenCalledWith(['./', node.id], jasmine.any(Object));
+ });
+
+ it('navigates to home when id not provided', () => {
+ component.navigate();
+
+ expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
+ });
+
+ it('it navigate home if node is root', () => {
+ (component).node = {
+ path: {
+ elements: [ {id: 'node-id'} ]
+ }
+ };
+
+ component.navigate(node.id);
+
+ expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
+ });
+ });
+});
diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts
new file mode 100644
index 000000000..0ff4f37a0
--- /dev/null
+++ b/src/app/components/files/files.component.ts
@@ -0,0 +1,191 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Observable, Subscription } from 'rxjs/Rx';
+import { Component, ViewChild, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
+import { Router, ActivatedRoute, Params } from '@angular/router';
+import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElementEntity, NodePaging } from 'alfresco-js-api';
+import { UploadService, FileUploadEvent, NodesApiService, AlfrescoContentService } from 'ng2-alfresco-core';
+
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { NodeActionsService } from '../../common/services/node-actions.service';
+
+import { PageComponent } from '../page.component';
+
+@Component({
+ templateUrl: './files.component.html'
+})
+export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
+ private routeData: any = {};
+
+ private onCopyNode: Subscription;
+ private onRemoveItem: Subscription;
+ private onCreateFolder: Subscription;
+ private onEditFolder: Subscription;
+ private onDeleteNode: Subscription;
+ private onMoveNode: Subscription;
+ private onRestoreNode: Subscription;
+ private onFileUploadComplete: Subscription;
+ private onToggleFavorite: Subscription;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private nodesApi: NodesApiService,
+ private changeDetector: ChangeDetectorRef,
+ private nodeActionsService: NodeActionsService,
+ private uploadService: UploadService,
+ private contentManagementService: ContentManagementService,
+ private browsingFilesService: BrowsingFilesService,
+ private contentService: AlfrescoContentService) {
+ super();
+ }
+
+ ngOnInit() {
+ const { route, contentManagementService, nodeActionsService, uploadService } = this;
+ const { data } = route.snapshot;
+
+ this.routeData = data;
+ this.title = data.i18nTitle;
+
+ route.params.subscribe(({ id }: Params) => {
+ const nodeId = id || data.defaultNodeId;
+ this.isLoading = true;
+
+ this.fetchNode(nodeId)
+ .do((node) => this.updateCurrentNode(node))
+ .flatMap((node) => {
+ return this.fetchNodes(node.id);
+ })
+ .subscribe(
+ (page) => this.onPageLoaded(page),
+ error => this.onFetchError(error)
+ );
+ });
+
+ this.onCopyNode = nodeActionsService.contentCopied
+ .subscribe((nodes) => this.onContentCopied(nodes));
+ this.onCreateFolder = contentManagementService.createFolder.subscribe(() => this.load());
+ this.onEditFolder = contentManagementService.editFolder.subscribe(() => this.load());
+ this.onDeleteNode = contentManagementService.deleteNode.subscribe(() => this.load());
+ this.onMoveNode = contentManagementService.moveNode.subscribe(() => this.load());
+ this.onRestoreNode = contentManagementService.restoreNode.subscribe(() => this.load());
+ this.onToggleFavorite = contentManagementService.toggleFavorite.subscribe(() => this.load());
+ this.onFileUploadComplete = uploadService.fileUploadComplete
+ .subscribe(file => this.onFileUploadedEvent(file));
+ this.onRemoveItem = uploadService.fileUploadDeleted
+ .subscribe((file) => this.onFileUploadedEvent(file));
+ }
+
+ ngOnDestroy() {
+ this.onCopyNode.unsubscribe();
+ this.onRemoveItem.unsubscribe();
+ this.onCreateFolder.unsubscribe();
+ this.onEditFolder.unsubscribe();
+ this.onDeleteNode.unsubscribe();
+ this.onMoveNode.unsubscribe();
+ this.onRestoreNode.unsubscribe();
+ this.onFileUploadComplete.unsubscribe();
+ this.onToggleFavorite.unsubscribe();
+
+ this.browsingFilesService.onChangeParent.next(null);
+ }
+
+ fetchNode(nodeId: string): Observable {
+ return this.nodesApi.getNode(nodeId);
+ }
+
+ fetchNodes(parentNodeId?: string, options: any = {}): Observable {
+ return this.nodesApi.getNodeChildren(parentNodeId, options);
+ }
+
+ navigate(nodeId: string = null) {
+ const commands = [ './' ];
+
+ if (nodeId && !this.isRootNode(nodeId)) {
+ commands.push(nodeId);
+ }
+
+ this.router.navigate(commands, {
+ relativeTo: this.route.parent
+ });
+ }
+
+ onNodeDoubleClick(node: MinimalNodeEntryEntity) {
+ if (node) {
+ if (node.isFolder) {
+ this.navigate(node.id);
+ }
+
+ if (node.isFile) {
+ this.router.navigate(['/preview', node.id]);
+ }
+ }
+ }
+
+ onBreadcrumbNavigate(route: PathElementEntity) {
+ this.navigate(route.id);
+ }
+
+ onFileUploadedEvent(event: FileUploadEvent) {
+ if (event && event.file.options.parentId === this.getParentNodeId()) {
+ this.load();
+ }
+ }
+
+ onContentCopied(nodes: MinimalNodeEntity[]) {
+ const newNode = nodes
+ .find((node) => {
+ return node && node.entry && node.entry.parentId === this.getParentNodeId();
+ });
+ if (newNode) {
+ this.load();
+ }
+ }
+
+ canCreateContent(parentNode: MinimalNodeEntryEntity): boolean {
+ if (parentNode) {
+ return this.contentService.hasPermission(parentNode, 'create');
+ }
+
+ return false;
+ }
+
+ load(showIndicator: boolean = false, pagination: any = {}) {
+ this.isLoading = showIndicator;
+
+ this.fetchNodes(this.getParentNodeId(), pagination)
+ .subscribe(
+ (page) => this.onPageLoaded(page),
+ error => this.onFetchError(error),
+ () => this.changeDetector.detectChanges()
+ );
+ }
+
+ private updateCurrentNode(node) {
+ this.node = node;
+ this.browsingFilesService.onChangeParent.next(node);
+ }
+
+ private isRootNode(nodeId: string): boolean {
+ if (this.node && this.node.path && this.node.path.elements && this.node.path.elements.length > 0) {
+ return this.node.path.elements[0].id === nodeId;
+ }
+ return false;
+ }
+}
diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html
new file mode 100644
index 000000000..15af2fe7d
--- /dev/null
+++ b/src/app/components/header/header.component.html
@@ -0,0 +1,15 @@
+
diff --git a/src/app/components/header/header.component.scss b/src/app/components/header/header.component.scss
new file mode 100644
index 000000000..6e7c1c419
--- /dev/null
+++ b/src/app/components/header/header.component.scss
@@ -0,0 +1,57 @@
+@import './../../ui/variables';
+
+.app-menu {
+
+ &.adf-toolbar {
+ .mat-toolbar {
+ background-color: #00bcd4;
+ font-family: 'Muli',"Roboto","Helvetica","Arial",sans-serif !important;
+ min-height: $app-menu-height;
+ height: $app-menu-height;
+
+ .mat-toolbar-layout {
+ height: $app-menu-height;
+
+ .mat-toolbar-row {
+ height: $app-menu-height;
+ }
+ }
+ }
+
+ .adf-toolbar-divider > div {
+ background-color: $alfresco-white !important;
+ }
+ }
+
+ .app-menu__title {
+ width: 100px;
+ height: $app-menu-height;
+
+ line-height: 54px;
+ text-decoration: none;
+ text-indent: -9999px;
+
+ color: inherit;
+
+ background: url('/assets/images/alfresco-logo-white.svg') no-repeat 0 50%;
+ background-size: 100% auto;
+
+ display: block;
+ position: relative;
+ font-size: 20px;
+ line-height: 1;
+ letter-spacing: .02em;
+ font-weight: 400;
+
+ &:after {
+ content: "Build #" attr(data-build-number);
+ color: rgba(white, .66);
+ font-size: 8px;
+ position: absolute;
+ bottom: 8px;
+ right: 2px;
+ line-height: 1;
+ text-indent: 0;
+ }
+ }
+}
diff --git a/src/app/components/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts
new file mode 100644
index 000000000..5b4497e8d
--- /dev/null
+++ b/src/app/components/header/header.component.spec.ts
@@ -0,0 +1,89 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CoreModule, AppConfigService, PeopleContentService } from 'ng2-alfresco-core';
+import { Observable } from 'rxjs/Rx';
+import { CommonModule } from './../../common/common.module';
+
+import { HeaderComponent } from './header.component';
+import { SearchComponent } from '../search/search.component';
+import { CurrentUserComponent } from '../current-user/current-user.component';
+
+describe('HeaderComponent', () => {
+ let fixture;
+ let component;
+ let appConfigService: AppConfigService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ HeaderComponent,
+ SearchComponent,
+ CurrentUserComponent
+ ]
+ })
+ .overrideProvider(PeopleContentService, {
+ useValue: {
+ getCurrentPerson: () => Observable.of({ entry: {} })
+ }
+ });
+
+ fixture = TestBed.createComponent(HeaderComponent);
+ component = fixture.componentInstance;
+ appConfigService = TestBed.get(AppConfigService);
+
+ spyOn(appConfigService, 'get').and.callFake((val) => {
+ if (val === 'application.name') {
+ return 'app-name';
+ }
+
+ if (val === 'application.build') {
+ return 'build-nr';
+ }
+ });
+
+ fixture.detectChanges();
+ });
+
+ it('get application name', () => {
+ expect(component.appName).toBe('app-name');
+ });
+
+ it('get application build number', () => {
+ expect(component.appBuildNumber).toBe('build-nr');
+ });
+
+ it('get application title', () => {
+ expect(component.appTitle).toContain('app-name');
+ expect(component.appTitle).toContain('build-nr');
+ });
+
+ it('toggle contrast', () => {
+ component.enhancedContrast = false;
+
+ component.toggleContrast();
+
+ expect(component.enhancedContrast).toBe(true);
+ });
+});
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts
new file mode 100644
index 000000000..5f24938c9
--- /dev/null
+++ b/src/app/components/header/header.component.ts
@@ -0,0 +1,49 @@
+/*!
+ * @license
+ * Copyright 2017 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, ViewEncapsulation } from '@angular/core';
+import { AppConfigService } from 'ng2-alfresco-core';
+
+@Component({
+ selector: 'app-header',
+ templateUrl: './header.component.html',
+ styleUrls: [ './header.component.scss' ],
+ encapsulation: ViewEncapsulation.None
+})
+export class HeaderComponent {
+ private enhancedContrast: Boolean = false;
+
+ constructor(private appConfig: AppConfigService) {}
+
+ get appName(): string {
+ return this.appConfig.get('application.name');
+ }
+
+ get appBuildNumber(): string {
+ return this.appConfig.get('application.build');
+ }
+
+ get appTitle(): string {
+ const { appName, appBuildNumber } = this;
+
+ return `${appName} (Build #${appBuildNumber})`;
+ }
+
+ toggleContrast() {
+ this.enhancedContrast = !this.enhancedContrast;
+ }
+}
diff --git a/src/app/components/layout/layout.component.html b/src/app/components/layout/layout.component.html
new file mode 100644
index 000000000..42fb479cf
--- /dev/null
+++ b/src/app/components/layout/layout.component.html
@@ -0,0 +1,15 @@
+
diff --git a/src/app/components/layout/layout.component.scss b/src/app/components/layout/layout.component.scss
new file mode 100644
index 000000000..c3aa6941d
--- /dev/null
+++ b/src/app/components/layout/layout.component.scss
@@ -0,0 +1,8 @@
+:host {
+ display: flex;
+ flex: 1;
+
+ router-outlet {
+ flex: 0 0;
+ }
+}
diff --git a/src/app/components/layout/layout.component.spec.ts b/src/app/components/layout/layout.component.spec.ts
new file mode 100644
index 000000000..e9950baed
--- /dev/null
+++ b/src/app/components/layout/layout.component.spec.ts
@@ -0,0 +1,104 @@
+/*!
+ * @license
+ * Copyright 2017 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 { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, ComponentFixture } from '@angular/core/testing';
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+import { CoreModule, AlfrescoContentService, PeopleContentService } from 'ng2-alfresco-core';
+import { Observable } from 'rxjs/Observable';
+
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+import { LayoutComponent } from './layout.component';
+import { CommonModule } from './../../common/common.module';
+import { HeaderComponent } from '../header/header.component';
+import { SidenavComponent } from '../sidenav/sidenav.component';
+import { SearchComponent } from '../search/search.component';
+import { CurrentUserComponent } from '../current-user/current-user.component';
+
+describe('LayoutComponent', () => {
+ let fixture: ComponentFixture;
+ let component: LayoutComponent;
+ let browsingFilesService: BrowsingFilesService;
+ let contentService: AlfrescoContentService;
+ let node;
+
+ beforeEach(() => {
+ node = { id: 'node-id' };
+
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ LayoutComponent,
+ HeaderComponent,
+ SidenavComponent,
+ SearchComponent,
+ CurrentUserComponent
+ ],
+ providers: [
+ {
+ provide: PeopleContentService,
+ useValue: {
+ getCurrentPerson: () => Observable.of({ entry: {} })
+ }
+ }
+ ]
+ });
+
+ fixture = TestBed.createComponent(LayoutComponent);
+ component = fixture.componentInstance;
+ browsingFilesService = TestBed.get(BrowsingFilesService);
+ contentService = TestBed.get(AlfrescoContentService);
+
+ fixture.detectChanges();
+ });
+
+ it('sets current node', () => {
+ const currentNode = { id: 'someId' };
+
+ browsingFilesService.onChangeParent.next(currentNode);
+
+ expect(component.node).toEqual(currentNode);
+ });
+
+ describe('canCreateContent()', () => {
+ it('returns true if node has permission', () => {
+ spyOn(contentService, 'hasPermission').and.returnValue(true);
+
+ const permission = component.canCreateContent({});
+
+ expect(permission).toBe(true);
+ });
+
+ it('returns false if node does not have permission', () => {
+ spyOn(contentService, 'hasPermission').and.returnValue(false);
+
+ const permission = component.canCreateContent({});
+
+ expect(permission).toBe(false);
+ });
+
+ it('returns false if node is null', () => {
+ const permission = component.canCreateContent(null);
+
+ expect(permission).toBe(false);
+ });
+ });
+ });
diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts
new file mode 100644
index 000000000..7d1d67bad
--- /dev/null
+++ b/src/app/components/layout/layout.component.ts
@@ -0,0 +1,53 @@
+/*!
+ * @license
+ * Copyright 2017 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, OnInit, OnDestroy } from '@angular/core';
+import { Subscription } from 'rxjs/Rx';
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+import { AlfrescoContentService } from 'ng2-alfresco-core';
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+
+@Component({
+ selector: 'app-layout',
+ templateUrl: './layout.component.html',
+ styleUrls: ['./layout.component.scss']
+})
+export class LayoutComponent implements OnInit, OnDestroy {
+ node: MinimalNodeEntryEntity;
+
+ browsingFilesSubscription: Subscription;
+
+ constructor(
+ private contentService: AlfrescoContentService,
+ private browsingFilesService: BrowsingFilesService) {}
+
+ ngOnInit() {
+ this.browsingFilesSubscription = this.browsingFilesService.onChangeParent
+ .subscribe((node: MinimalNodeEntryEntity) => this.node = node);
+ }
+
+ ngOnDestroy() {
+ this.browsingFilesSubscription.unsubscribe();
+ }
+
+ canCreateContent(node: MinimalNodeEntryEntity): boolean {
+ if (node) {
+ return this.contentService.hasPermission(node, 'create');
+ }
+ return false;
+ }
+}
diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html
new file mode 100644
index 000000000..98524e8d0
--- /dev/null
+++ b/src/app/components/libraries/libraries.component.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ makeLibraryTitle(context.row.obj.entry) }}
+
+
+
+
+
+
+
+ {{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}
+
+
+ {{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}
+
+
+ {{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}
+
+
+
+
+
+
+
diff --git a/src/app/components/libraries/libraries.component.spec.ts b/src/app/components/libraries/libraries.component.spec.ts
new file mode 100644
index 000000000..d869760e7
--- /dev/null
+++ b/src/app/components/libraries/libraries.component.spec.ts
@@ -0,0 +1,189 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, async } from '@angular/core/testing';
+import { Observable } from 'rxjs/Rx';
+
+import { CoreModule , NodesApiService, AlfrescoApiService} from 'ng2-alfresco-core';
+
+import { CommonModule } from '../../common/common.module';
+import { LibrariesComponent } from './libraries.component';
+
+describe('Libraries Routed Component', () => {
+ let fixture;
+ let component: LibrariesComponent;
+ let nodesApi: NodesApiService;
+ let alfrescoApi: AlfrescoApiService;
+ let router: Router;
+ let page;
+ let node;
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
+ pagination: { data: 'data'}
+ }
+ };
+
+ node = {
+ id: 'nodeId',
+ path: {
+ elements: []
+ }
+ };
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ LibrariesComponent
+ ]
+ })
+ .compileComponents().then(() => {
+ fixture = TestBed.createComponent(LibrariesComponent);
+ component = fixture.componentInstance;
+
+ nodesApi = TestBed.get(NodesApiService);
+ alfrescoApi = TestBed.get(AlfrescoApiService);
+ router = TestBed.get(Router);
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(alfrescoApi.sitesApi, 'getSites').and.returnValue((Promise.resolve(page)));
+ });
+
+ describe('makeLibraryTooltip()', () => {
+ it('maps tooltip to description', () => {
+ node.description = 'description';
+ const tooltip = component.makeLibraryTooltip(node);
+
+ expect(tooltip).toBe(node.description);
+ });
+
+ it('maps tooltip to description', () => {
+ node.title = 'title';
+ const tooltip = component.makeLibraryTooltip(node);
+
+ expect(tooltip).toBe(node.title);
+ });
+
+ it('sets tooltip to empty string', () => {
+ const tooltip = component.makeLibraryTooltip(node);
+
+ expect(tooltip).toBe('');
+ });
+ });
+
+ describe('makeLibraryTitle()', () => {
+ it('sets title with id when duplicate nodes title exists in list', () => {
+ node.title = 'title';
+
+ component.documentList.node = {
+ list: {
+ entries: [{ entry: { id: 'some-id', title: 'title' } }]
+ }
+ };
+
+ const title = component.makeLibraryTitle(node);
+
+ expect(title).toContain('nodeId');
+ });
+
+ it('sets title when no duplicate nodes title exists in list', () => {
+ node.title = 'title';
+
+ component.paging = {
+ list: {
+ entries: [{ entry: { id: 'some-id', title: 'title-some-id' } }]
+ }
+ };
+
+ const title = component.makeLibraryTitle(node);
+
+ expect(title).toBe('title');
+ });
+ });
+
+ describe('Node navigation', () => {
+ let routerSpy;
+
+ beforeEach(() => {
+ routerSpy = spyOn(router, 'navigate');
+ spyOn(component, 'fetchNodes').and.callFake(val => val);
+ });
+
+ it('does not navigate when id is not passed', () => {
+ component.navigate(null);
+
+ expect(router.navigate).not.toHaveBeenCalled();
+ });
+
+ it('navigates to node id', () => {
+ const document = { id: 'documentId' };
+ spyOn(nodesApi, 'getNode').and.returnValue(Observable.of(document));
+
+ component.navigate(node.id);
+
+ fixture.detectChanges();
+
+ expect(routerSpy.calls.argsFor(0)[0]).toEqual(['./', document.id]);
+ });
+ });
+
+ describe('onNodeDoubleClick', () => {
+ it('navigates to document', () => {
+ spyOn(component, 'navigate');
+
+ const event: any = {
+ detail: {
+ node: {
+ entry: { guid: 'node-guid' }
+ }
+ }
+ };
+
+ component.onNodeDoubleClick(event);
+
+ expect(component.navigate).toHaveBeenCalledWith('node-guid');
+ });
+
+ it(' does not navigate when document is not provided', () => {
+ spyOn(component, 'navigate');
+
+ const event: any = {
+ detail: {
+ node: {
+ entry: null
+ }
+ }
+ };
+
+ component.onNodeDoubleClick(event);
+
+ expect(component.navigate).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/libraries/libraries.component.ts b/src/app/components/libraries/libraries.component.ts
new file mode 100644
index 000000000..e95fe8e95
--- /dev/null
+++ b/src/app/components/libraries/libraries.component.ts
@@ -0,0 +1,80 @@
+/*!
+ * @license
+ * Copyright 2017 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, ViewChild } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { NodesApiService } from 'ng2-alfresco-core';
+
+import { PageComponent } from '../page.component';
+import { DocumentListComponent } from 'ng2-alfresco-documentlist';
+
+@Component({
+ templateUrl: './libraries.component.html'
+})
+export class LibrariesComponent extends PageComponent {
+
+ @ViewChild(DocumentListComponent)
+ documentList: DocumentListComponent;
+
+ constructor(
+ private nodesApi: NodesApiService,
+ private route: ActivatedRoute,
+ private router: Router) {
+ super();
+ }
+
+ makeLibraryTooltip(library: any): string {
+ const { description, title } = library;
+
+ return description || title || '';
+ }
+
+ makeLibraryTitle(library: any): string {
+ const { title, id } = library;
+ let isDuplicate = false;
+ if (this.documentList.node) {
+ isDuplicate = this.documentList.node.list.entries
+ .some(({ entry }: any) => {
+ return (entry.id !== id && entry.title === title);
+ });
+ }
+
+ return isDuplicate ? `${title} (${id})` : `${title}`;
+ }
+
+ onNodeDoubleClick(e: CustomEvent) {
+ const node: any = e.detail.node.entry;
+
+ if (node && node.guid) {
+ this.navigate(node.guid);
+ }
+ }
+
+ navigate(libraryId: string) {
+ if (libraryId) {
+ this.nodesApi
+ .getNode(libraryId, { relativePath: '/documentLibrary' })
+ .subscribe(documentLibrary => {
+ this.router.navigate([ './', documentLibrary.id ], { relativeTo: this.route });
+ });
+ }
+ }
+
+ fetchNodes(): void {
+ // todo: remove once all views migrate to native data source
+ }
+}
diff --git a/src/app/components/login/login.component.spec.ts b/src/app/components/login/login.component.spec.ts
index 1580b1e82..5431dc0e9 100644
--- a/src/app/components/login/login.component.spec.ts
+++ b/src/app/components/login/login.component.spec.ts
@@ -25,120 +25,120 @@ import { LoginModule } from 'ng2-alfresco-login';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
- let router: Router;
- let route: ActivatedRoute;
- let authService: AlfrescoAuthenticationService;
- let userPrefService: UserPreferencesService;
+ let router: Router;
+ let route: ActivatedRoute;
+ let authService: AlfrescoAuthenticationService;
+ let userPrefService: UserPreferencesService;
- class TestConfig {
- private testBed;
- private componentInstance;
- private fixture;
+ class TestConfig {
+ private testBed;
+ private componentInstance;
+ private fixture;
- constructor(config: any = {}) {
- const routerProvider = {
- provide: Router,
- useValue: {
- navigateByUrl: jasmine.createSpy('navigateByUrl'),
- navigate: jasmine.createSpy('navigate')
+ constructor(config: any = {}) {
+ const routerProvider = {
+ provide: Router,
+ useValue: {
+ navigateByUrl: jasmine.createSpy('navigateByUrl'),
+ navigate: jasmine.createSpy('navigate')
+ }
+ };
+
+ const authProvider = {
+ provide: AlfrescoAuthenticationService,
+ useValue: {
+ isEcmLoggedIn: jasmine.createSpy('navigateByUrl')
+ .and.returnValue(config.isEcmLoggedIn || false)
+ }
+ };
+
+ const preferencesProvider = {
+ provide: UserPreferencesService,
+ useValue: {
+ setStoragePrefix: jasmine.createSpy('setStoragePrefix')
+ }
+ };
+
+ this.testBed = TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ LoginModule
+ ],
+ declarations: [
+ LoginComponent
+ ],
+ providers: [
+ routerProvider,
+ authProvider,
+ preferencesProvider,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ params: Observable.of({ redirect: config.redirect })
+ }
+ }
+ ]
+ });
+
+ this.fixture = TestBed.createComponent(LoginComponent);
+ this.componentInstance = this.fixture.componentInstance;
+ this.fixture.detectChanges();
}
- };
- const authProvider = {
- provide: AlfrescoAuthenticationService,
- useValue: {
- isEcmLoggedIn: jasmine.createSpy('navigateByUrl')
- .and.returnValue(config.isEcmLoggedIn || false)
+ get userPrefService() {
+ return TestBed.get(UserPreferencesService);
}
- };
- const preferencesProvider = {
- provide: UserPreferencesService,
- useValue: {
- setStoragePrefix: jasmine.createSpy('setStoragePrefix')
+ get authService() {
+ return TestBed.get(AlfrescoAuthenticationService);
}
- };
- this.testBed = TestBed.configureTestingModule({
- imports: [
- RouterTestingModule,
- LoginModule
- ],
- declarations: [
- LoginComponent
- ],
- providers: [
- routerProvider,
- authProvider,
- preferencesProvider,
- {
- provide: ActivatedRoute,
- useValue: {
- params: Observable.of({ redirect: config.redirect })
- }
- }
- ]
- });
+ get routerService() {
+ return TestBed.get(Router);
+ }
- this.fixture = TestBed.createComponent(LoginComponent);
- this.componentInstance = this.fixture.componentInstance;
- this.fixture.detectChanges();
+ get component() {
+ return this.componentInstance;
+ }
}
- get userPrefService() {
- return TestBed.get(UserPreferencesService);
- }
+ it('load app when user is already logged in', () => {
+ const testConfig = new TestConfig({
+ isEcmLoggedIn: true
+ });
- get authService() {
- return TestBed.get(AlfrescoAuthenticationService);
- }
-
- get routerService() {
- return TestBed.get(Router);
- }
-
- get component() {
- return this.componentInstance;
- }
- }
-
- it('load app when user is already logged in', () => {
- const testConfig = new TestConfig({
- isEcmLoggedIn: true
+ expect(testConfig.routerService.navigateByUrl).toHaveBeenCalled();
});
- expect(testConfig.routerService.navigateByUrl).toHaveBeenCalled();
- });
+ it('requires user to be logged in', () => {
+ const testConfig = new TestConfig({
+ isEcmLoggedIn: false,
+ redirect: '/personal-files'
+ });
- it('requires user to be logged in', () => {
- const testConfig = new TestConfig({
- isEcmLoggedIn: false,
- redirect: '/personal-files'
+ expect(testConfig.routerService.navigate).toHaveBeenCalledWith(['/login', {}]);
});
- expect(testConfig.routerService.navigate).toHaveBeenCalledWith(['/login', {}]);
- });
+ describe('onLoginSuccess()', () => {
+ let testConfig;
- describe('onLoginSuccess()', () => {
- let testConfig;
+ beforeEach(() => {
+ testConfig = new TestConfig({
+ isEcmLoggedIn: false,
+ redirect: 'somewhere-over-the-rainbow'
+ });
+ });
- beforeEach(() => {
- testConfig = new TestConfig({
- isEcmLoggedIn: false,
- redirect: 'somewhere-over-the-rainbow'
- });
+ it('redirects on success', () => {
+ testConfig.component.onLoginSuccess();
+
+ expect(testConfig.routerService.navigateByUrl).toHaveBeenCalledWith('somewhere-over-the-rainbow');
+ });
+
+ it('sets user preference store prefix', () => {
+ testConfig.component.onLoginSuccess({ username: 'bogus' });
+
+ expect(testConfig.userPrefService.setStoragePrefix).toHaveBeenCalledWith('bogus');
+ });
});
-
- it('redirects on success', () => {
- testConfig.component.onLoginSuccess();
-
- expect(testConfig.routerService.navigateByUrl).toHaveBeenCalledWith('somewhere-over-the-rainbow');
- });
-
- it('sets user preference store prefix', () => {
- testConfig.component.onLoginSuccess({ username: 'bogus' });
-
- expect(testConfig.userPrefService.setStoragePrefix).toHaveBeenCalledWith('bogus');
- });
- });
});
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts
index bd0713c51..87413a876 100644
--- a/src/app/components/login/login.component.ts
+++ b/src/app/components/login/login.component.ts
@@ -22,48 +22,48 @@ import { Validators } from '@angular/forms';
import { AlfrescoAuthenticationService, UserPreferencesService } from 'ng2-alfresco-core';
const skipRedirectUrls: string[] = [
- '/logout',
- '/personal-files'
+ '/logout',
+ '/personal-files'
];
@Component({
- templateUrl: './login.component.html'
+ templateUrl: './login.component.html'
})
export class LoginComponent {
- private redirectUrl = '';
+ private redirectUrl = '';
- constructor(
- private router: Router,
- private route: ActivatedRoute,
- private auth: AlfrescoAuthenticationService,
- private userPreferences: UserPreferencesService
- ) {
- if (auth.isEcmLoggedIn()) {
- this.redirect();
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private auth: AlfrescoAuthenticationService,
+ private userPreferences: UserPreferencesService
+ ) {
+ if (auth.isEcmLoggedIn()) {
+ this.redirect();
+ }
+
+ route.params.subscribe((params: any) => {
+ if (skipRedirectUrls.indexOf(params.redirect) > -1) {
+ const remainingParams = Object.assign({}, params);
+
+ delete remainingParams.redirect;
+
+ router.navigate(['/login', remainingParams]);
+ }
+
+ this.redirectUrl = params.redirect;
+ });
}
- route.params.subscribe((params: any) => {
- if (skipRedirectUrls.indexOf(params.redirect) > -1) {
- const remainingParams = Object.assign({}, params);
-
- delete remainingParams.redirect;
-
- router.navigate(['/login', remainingParams]);
- }
-
- this.redirectUrl = params.redirect;
- });
- }
-
- redirect() {
- this.router.navigateByUrl(this.redirectUrl || '');
- }
-
- onLoginSuccess(data) {
- if (data && data.username) {
- this.userPreferences.setStoragePrefix(data.username);
+ redirect() {
+ this.router.navigateByUrl(this.redirectUrl || '');
+ }
+
+ onLoginSuccess(data) {
+ if (data && data.username) {
+ this.userPreferences.setStoragePrefix(data.username);
+ }
+ this.redirect();
}
- this.redirect();
- }
}
diff --git a/src/app/components/page.component.spec.ts b/src/app/components/page.component.spec.ts
new file mode 100644
index 000000000..fa45ed439
--- /dev/null
+++ b/src/app/components/page.component.spec.ts
@@ -0,0 +1,295 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed } from '@angular/core/testing';
+
+import { PageComponent } from './page.component';
+
+class TestClass extends PageComponent {
+ node: any;
+
+ constructor() {
+ super();
+ }
+
+ fetchNodes(parentNodeId?: string, options?: any) {
+ // abstract
+ }
+}
+
+describe('PageComponent', () => {
+ let component;
+
+ beforeEach(() => {
+ component = new TestClass();
+ });
+
+ describe('getParentNodeId()', () => {
+ it('returns parent node id when node is set', () => {
+ component.node = { id: 'node-id' };
+
+ expect(component.getParentNodeId()).toBe('node-id');
+ });
+
+ it('returns null when node is not set', () => {
+ component.node = null;
+
+ expect(component.getParentNodeId()).toBe(null);
+ });
+ });
+
+ describe('onFetchError()', () => {
+ it('sets isLoading state to false', () => {
+ component.isLoading = true;
+
+ component.onFetchError();
+
+ expect(component.isLoading).toBe(false);
+ });
+ });
+
+ describe('onPaginationChange()', () => {
+ it('fetch children nodes for current node id', () => {
+ component.node = { id: 'node-id' };
+ spyOn(component, 'fetchNodes').and.stub();
+
+ component.onPaginationChange({pagination: 'pagination-data'});
+
+ expect(component.fetchNodes).toHaveBeenCalledWith('node-id', { pagination: 'pagination-data' });
+ });
+ });
+
+ describe('onPageLoaded()', () => {
+ let page;
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: ['a', 'b', 'c'],
+ pagination: {}
+ }
+ };
+
+ component.isLoading = true;
+ component.isEmpty = true;
+ component.onPageLoaded(page);
+ });
+
+ it('sets isLoading state to false', () => {
+ expect(component.isLoading).toBe(false);
+ });
+
+ it('sets component paging data', () => {
+ expect(component.paging).toBe(page);
+ });
+
+ it('sets component pagination data', () => {
+ expect(component.pagination).toBe(page.list.pagination);
+ });
+
+ it('sets component isEmpty state', () => {
+ expect(component.isEmpty).toBe(false);
+ });
+ });
+
+ describe('hasSelection()', () => {
+ it('returns true when it has nodes selected', () => {
+ const hasSelection = component.hasSelection([ {}, {} ]);
+ expect(hasSelection).toBe(true);
+ });
+
+ it('returns false when it has no selections', () => {
+ const hasSelection = component.hasSelection([]);
+ expect(hasSelection).toBe(false);
+ });
+ });
+
+ describe('filesOnlySelected()', () => {
+ it('return true if only files are selected', () => {
+ const selected = [ { entry: { isFile: true } }, { entry: { isFile: true } } ];
+ expect(component.filesOnlySelected(selected)).toBe(true);
+ });
+
+ it('return false if selection contains others types', () => {
+ const selected = [ { entry: { isFile: true } }, { entry: { isFolder: true } } ];
+ expect(component.filesOnlySelected(selected)).toBe(false);
+ });
+
+ it('return false if selection contains no files', () => {
+ const selected = [ { entry: { isFolder: true } } ];
+ expect(component.filesOnlySelected(selected)).toBe(false);
+ });
+
+ it('return false no selection', () => {
+ const selected = [];
+ expect(component.filesOnlySelected(selected)).toBe(false);
+ });
+ });
+
+ describe('foldersOnlySelected()', () => {
+ it('return true if only folders are selected', () => {
+ const selected = [ { entry: { isFolder: true } }, { entry: { isFolder: true } } ];
+ expect(component.foldersOnlySelected(selected)).toBe(true);
+ });
+
+ it('return false if selection contains others types', () => {
+ const selected = [ { entry: { isFile: true } }, { entry: { isFolder: true } } ];
+ expect(component.foldersOnlySelected(selected)).toBe(false);
+ });
+
+ it('return false if selection contains no files', () => {
+ const selected = [ { entry: { isFile: true } } ];
+ expect(component.foldersOnlySelected(selected)).toBe(false);
+ });
+
+ it('return false no selection', () => {
+ const selected = [];
+ expect(component.foldersOnlySelected(selected)).toBe(false);
+ });
+ });
+
+ describe('isFileSelected()', () => {
+ it('returns true if selected node is file', () => {
+ const selection = [ { entry: { isFile: true } } ];
+ expect(component.isFileSelected(selection)).toBe(true);
+ });
+
+ it('returns false if selected node is folder', () => {
+ const selection = [ { entry: { isFolder: true } } ];
+ expect(component.isFileSelected(selection)).toBe(false);
+ });
+
+ it('returns false if there are more than one selections', () => {
+ const selection = [ { entry: { isFile: true } }, { entry: { isFile: true } } ];
+ expect(component.isFileSelected(selection)).toBe(false);
+ });
+ });
+
+ describe('canEditFolder()', () => {
+ it('returns true if selected node is folder', () => {
+ const selection = [ { entry: { isFolder: true } } ];
+ spyOn(component, 'nodeHasPermission').and.returnValue(true);
+
+ expect(component.canEditFolder(selection)).toBe(true);
+ });
+
+ it('returns false if selected node is file', () => {
+ const selection = [ { entry: { isFile: true } } ];
+ expect(component.canEditFolder(selection)).toBe(false);
+ });
+
+ it('returns false if there are more than one selections', () => {
+ const selection = [ { entry: { isFolder: true } }, { entry: { isFolder: true } } ];
+ expect(component.canEditFolder(selection)).toBe(false);
+ });
+
+ it('returns false folder dows not have edit permission', () => {
+ spyOn(component, 'nodeHasPermission').and.returnValue(false);
+ const selection = [ { entry: { isFolder: true } } ];
+
+ expect(component.canEditFolder(selection)).toBe(false);
+ });
+ });
+
+ describe('canDelete()', () => {
+ it('returns false if node has no delete permission', () => {
+ const selection = [ { entry: { } } ];
+ spyOn(component, 'nodeHasPermission').and.returnValue(false);
+
+ expect(component.canDelete(selection)).toBe(false);
+ });
+
+ it('returns true if node has delete permission', () => {
+ const selection = [ { entry: { } } ];
+ spyOn(component, 'nodeHasPermission').and.returnValue(true);
+
+ expect(component.canDelete(selection)).toBe(true);
+ });
+ });
+
+ describe('canMove()', () => {
+ it('returns true if node can be deleted', () => {
+ const selection = [ { entry: { } } ];
+ spyOn(component, 'canDelete').and.returnValue(true);
+
+ expect(component.canMove(selection)).toBe(true);
+ });
+
+ it('returns false if node can not be deleted', () => {
+ const selection = [ { entry: { } } ];
+ spyOn(component, 'canDelete').and.returnValue(false);
+
+ expect(component.canMove(selection)).toBe(false);
+ });
+ });
+
+ describe('canPreviewFile()', () => {
+ it('it returns true if node is file', () => {
+ const selection = [{ entry: { isFile: true } }];
+
+ expect(component.canPreviewFile(selection)).toBe(true);
+ });
+
+ it('it returns false if node is folder', () => {
+ const selection = [{ entry: { isFolder: true } }];
+
+ expect(component.canPreviewFile(selection)).toBe(false);
+ });
+ });
+
+ describe('canShareFile()', () => {
+ it('it returns true if node is file', () => {
+ const selection = [{ entry: { isFile: true } }];
+
+ expect(component.canShareFile(selection)).toBe(true);
+ });
+
+ it('it returns false if node is folder', () => {
+ const selection = [{ entry: { isFolder: true } }];
+
+ expect(component.canShareFile(selection)).toBe(false);
+ });
+ });
+
+ describe('canDownloadFile()', () => {
+ it('it returns true if node is file', () => {
+ const selection = [{ entry: { isFile: true } }];
+
+ expect(component.canDownloadFile(selection)).toBe(true);
+ });
+
+ it('it returns false if node is folder', () => {
+ const selection = [{ entry: { isFolder: true } }];
+
+ expect(component.canDownloadFile(selection)).toBe(false);
+ });
+ });
+
+ describe('nodeHasPermission()', () => {
+ it('returns true is has permission', () => {
+ const node = { allowableOperations: ['some-operation'] };
+
+ expect(component.nodeHasPermission(node, 'some-operation')).toBe(true);
+ });
+
+ it('returns true is has permission', () => {
+ const node = { allowableOperations: ['other-operation'] };
+
+ expect(component.nodeHasPermission(node, 'some-operation')).toBe(false);
+ });
+ });
+});
diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts
new file mode 100644
index 000000000..59fcb0abc
--- /dev/null
+++ b/src/app/components/page.component.ts
@@ -0,0 +1,124 @@
+/*!
+ * @license
+ * Copyright 2017 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 { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
+
+export abstract class PageComponent {
+
+ title: string = 'Page';
+
+ isLoading: boolean = false;
+ isEmpty: boolean = true;
+
+ paging: NodePaging;
+ pagination: Pagination;
+
+ node: MinimalNodeEntryEntity;
+
+ abstract fetchNodes(parentNodeId?: string, options?: any): void;
+
+ onFetchError(error: any) {
+ this.isLoading = false;
+ }
+
+ getParentNodeId(): string {
+ return this.node ? this.node.id : null;
+ }
+
+ onPaginationChange(pagination: any) {
+ this.fetchNodes(this.getParentNodeId(), pagination);
+ }
+
+ onPageLoaded(page: NodePaging) {
+ this.isLoading = false;
+ this.paging = page;
+ this.pagination = page.list.pagination;
+ this.isEmpty = !(page.list.entries && page.list.entries.length > 0);
+ }
+
+ hasSelection(selection: Array): boolean {
+ return selection && selection.length > 0;
+ }
+
+ filesOnlySelected(selection: Array): boolean {
+ if (this.hasSelection(selection)) {
+ return selection.every(entity => entity.entry && entity.entry.isFile);
+ }
+ return false;
+ }
+
+ foldersOnlySelected(selection: Array): boolean {
+ if (this.hasSelection(selection)) {
+ return selection.every(entity => entity.entry && entity.entry.isFolder);
+ }
+ return false;
+ }
+
+ isFileSelected(selection: Array): boolean {
+ if (selection && selection.length === 1) {
+ let entry = selection[0].entry;
+
+ if (entry && entry.isFile) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ canEditFolder(selection: Array): boolean {
+ if (selection && selection.length === 1) {
+ let entry = selection[0].entry;
+
+ if (entry && entry.isFolder) {
+ return this.nodeHasPermission(entry, 'update');
+ }
+ }
+ return false;
+ }
+
+ canDelete(selection: Array = []): boolean {
+ return selection.every(node => node.entry && this.nodeHasPermission(node.entry, 'delete'));
+ }
+
+ canMove(selection: Array): boolean {
+ return this.canDelete(selection);
+ }
+
+ canPreviewFile(selection: Array): boolean {
+ return this.isFileSelected(selection);
+ }
+
+ canShareFile(selection: Array): boolean {
+ return this.isFileSelected(selection);
+ }
+
+ canDownloadFile(selection: Array): boolean {
+ return this.isFileSelected(selection);
+ }
+
+ nodeHasPermission(node: MinimalNodeEntryEntity, permission: string) {
+ if (node && permission) {
+ const { allowableOperations = [] } = (node || {});
+
+ if (allowableOperations.indexOf(permission) > -1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/app/components/preview/preview.component.html b/src/app/components/preview/preview.component.html
new file mode 100644
index 000000000..2592514c9
--- /dev/null
+++ b/src/app/components/preview/preview.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/app/components/preview/preview.component.scss b/src/app/components/preview/preview.component.scss
new file mode 100644
index 000000000..6a2cae035
--- /dev/null
+++ b/src/app/components/preview/preview.component.scss
@@ -0,0 +1,4 @@
+.app-preview {
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts
new file mode 100644
index 000000000..b6f63bf4e
--- /dev/null
+++ b/src/app/components/preview/preview.component.ts
@@ -0,0 +1,56 @@
+/*!
+ * @license
+ * Copyright 2017 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, OnInit, ViewEncapsulation } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AlfrescoApiService } from 'ng2-alfresco-core';
+
+@Component({
+ selector: 'app-preview',
+ templateUrl: 'preview.component.html',
+ styleUrls: ['preview.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ host: { 'class': 'app-preview' }
+})
+export class PreviewComponent implements OnInit {
+
+ nodeId: string = null;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private apiService: AlfrescoApiService) {}
+
+ ngOnInit() {
+ this.route.params.subscribe(params => {
+ const id = params.nodeId;
+ if (id) {
+ this.apiService.getInstance().nodes.getNodeInfo(id).then(
+ (node) => {
+ if (node && node.isFile) {
+ this.nodeId = id;
+ return;
+ }
+ this.router.navigate(['/personal-files', id]);
+ },
+ () => this.router.navigate(['/personal-files', id])
+ );
+ }
+ });
+ }
+
+}
diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html
new file mode 100644
index 000000000..a70a9e526
--- /dev/null
+++ b/src/app/components/recent-files/recent-files.component.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/recent-files/recent-files.component.spec.ts b/src/app/components/recent-files/recent-files.component.spec.ts
new file mode 100644
index 000000000..ba1defdc5
--- /dev/null
+++ b/src/app/components/recent-files/recent-files.component.spec.ts
@@ -0,0 +1,152 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed, async } from '@angular/core/testing';
+import { Observable } from 'rxjs/Rx';
+
+import { CoreModule, AlfrescoApiService } from 'ng2-alfresco-core';
+
+import { CommonModule } from '../../common/common.module';
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { RecentFilesComponent } from './recent-files.component';
+
+describe('RecentFiles Routed Component', () => {
+ let fixture;
+ let component;
+ let router: Router;
+ let alfrescoApi: AlfrescoApiService;
+ let contentService: ContentManagementService;
+ let page;
+ let person;
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
+ pagination: { data: 'data'}
+ }
+ };
+
+ person = { entry: { id: 'bogus' } };
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ RecentFilesComponent
+ ]
+ })
+ .compileComponents().then(() => {
+ fixture = TestBed.createComponent(RecentFilesComponent);
+ component = fixture.componentInstance;
+
+ router = TestBed.get(Router);
+ contentService = TestBed.get(ContentManagementService);
+ alfrescoApi = TestBed.get(AlfrescoApiService);
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue(Promise.resolve({
+ entry: { id: 'personId' }
+ }));
+
+ spyOn(alfrescoApi.searchApi, 'search').and.returnValue(Promise.resolve(page));
+ });
+
+ describe('OnInit()', () => {
+ beforeEach(() => {
+ spyOn(component, 'refresh').and.stub();
+ });
+
+ it('should reload nodes on onDeleteNode event', () => {
+ fixture.detectChanges();
+
+ contentService.deleteNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should reload on onRestoreNode event', () => {
+ fixture.detectChanges();
+
+ contentService.restoreNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should reload on toggleFavorite event', () => {
+ fixture.detectChanges();
+
+ contentService.toggleFavorite.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should reload on move node event', () => {
+ fixture.detectChanges();
+
+ contentService.moveNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+ });
+
+ describe('onNodeDoubleClick()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNodes').and.callFake(val => val);
+ });
+
+ it('open preview if node is file', () => {
+ spyOn(router, 'navigate').and.stub();
+ const node: any = { isFile: true };
+
+ component.onNodeDoubleClick(node);
+ fixture.detectChanges();
+
+ expect(router.navigate).toHaveBeenCalledWith(['/preview', node.id]);
+ });
+
+ it('does not open preview if node is folder', () => {
+ spyOn(router, 'navigate').and.stub();
+ const node: any = { isFolder: true };
+
+ component.onNodeDoubleClick(node);
+ fixture.detectChanges();
+
+ expect(router.navigate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('refresh', () => {
+ it('should call document list reload', () => {
+ spyOn(component.documentList, 'reload');
+ fixture.detectChanges();
+
+ component.refresh();
+
+ expect(component.documentList.reload).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/recent-files/recent-files.component.ts b/src/app/components/recent-files/recent-files.component.ts
new file mode 100644
index 000000000..4bbe2d546
--- /dev/null
+++ b/src/app/components/recent-files/recent-files.component.ts
@@ -0,0 +1,75 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Subscription } from 'rxjs/Rx';
+import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
+import { Router } from '@angular/router';
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+import { DocumentListComponent } from 'ng2-alfresco-documentlist';
+
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { PageComponent } from '../page.component';
+
+@Component({
+ templateUrl: './recent-files.component.html'
+})
+export class RecentFilesComponent extends PageComponent implements OnInit, OnDestroy {
+
+ @ViewChild(DocumentListComponent)
+ documentList: DocumentListComponent;
+
+ private onDeleteNode: Subscription;
+ private onMoveNode: Subscription;
+ private onRestoreNode: Subscription;
+ private onToggleFavorite: Subscription;
+
+ constructor(
+ private router: Router,
+ private content: ContentManagementService) {
+ super();
+ }
+
+ ngOnInit() {
+ this.onDeleteNode = this.content.deleteNode.subscribe(() => this.refresh());
+ this.onMoveNode = this.content.moveNode.subscribe(() => this.refresh());
+ this.onRestoreNode = this.content.restoreNode.subscribe(() => this.refresh());
+ this.onToggleFavorite = this.content.toggleFavorite.subscribe(() => this.refresh());
+ }
+
+ ngOnDestroy() {
+ this.onDeleteNode.unsubscribe();
+ this.onMoveNode.unsubscribe();
+ this.onRestoreNode.unsubscribe();
+ this.onToggleFavorite.unsubscribe();
+ }
+
+ onNodeDoubleClick(node: MinimalNodeEntryEntity) {
+ if (node && node.isFile) {
+ this.router.navigate(['/preview', node.id]);
+ }
+ }
+
+ fetchNodes(): void {
+ // todo: remove once all views migrate to native data source
+ }
+
+ refresh(): void {
+ if (this.documentList) {
+ this.documentList.reload();
+ }
+ }
+}
diff --git a/src/app/components/search/search.component.html b/src/app/components/search/search.component.html
new file mode 100644
index 000000000..516b78e5d
--- /dev/null
+++ b/src/app/components/search/search.component.html
@@ -0,0 +1,6 @@
+
+
diff --git a/src/app/components/search/search.component.scss b/src/app/components/search/search.component.scss
new file mode 100644
index 000000000..580754e74
--- /dev/null
+++ b/src/app/components/search/search.component.scss
@@ -0,0 +1,9 @@
+@import './../../ui/variables';
+
+adf-search-control {
+ color: $alfresco-white;
+}
+
+:host {
+ height: $app-menu-height;
+}
diff --git a/src/app/components/search/search.component.spec.ts b/src/app/components/search/search.component.spec.ts
new file mode 100644
index 000000000..09f9d23e7
--- /dev/null
+++ b/src/app/components/search/search.component.spec.ts
@@ -0,0 +1,71 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, async } from '@angular/core/testing';
+import { CoreModule, AppConfigService } from 'ng2-alfresco-core';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SearchComponent } from './search.component';
+import { CommonModule } from './../../common/common.module';
+
+describe('SearchComponent', () => {
+ let fixture;
+ let component;
+ let router: Router;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ SearchComponent
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(SearchComponent);
+ component = fixture.componentInstance;
+ router = TestBed.get(Router);
+
+ fixture.detectChanges();
+ });
+ }));
+
+ describe('onNodeClicked()', () => {
+ it('opens preview if node is file', () => {
+ spyOn(router, 'navigate').and.stub();
+ const node = { entry: { isFile: true, id: 'node-id' } };
+
+ component.onNodeClicked(node);
+
+ expect(router.navigate).toHaveBeenCalledWith(['/preview', node.entry.id]);
+ });
+
+ it('navigates if node is folder', () => {
+ const node = { entry: { isFolder: true } };
+ spyOn(router, 'navigate');
+
+ component.onNodeClicked(node);
+
+ expect(router.navigate).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/search/search.component.ts b/src/app/components/search/search.component.ts
new file mode 100644
index 000000000..640b39091
--- /dev/null
+++ b/src/app/components/search/search.component.ts
@@ -0,0 +1,45 @@
+/*!
+ * @license
+ * Copyright 2017 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 } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { MinimalNodeEntity } from 'alfresco-js-api';
+
+@Component({
+ selector: 'app-search',
+ templateUrl: 'search.component.html',
+ styleUrls: ['search.component.scss']
+})
+export class SearchComponent {
+
+ searchTerm: string = '';
+
+ constructor(
+ private router: Router) {
+ }
+
+ onNodeClicked(node: MinimalNodeEntity) {
+ if (node && node.entry) {
+ if (node.entry.isFile) {
+ this.router.navigate(['/preview', node.entry.id]);
+ } else if (node.entry.isFolder) {
+ this.router.navigate([ '/personal-files', node.entry.id ]);
+ }
+ }
+ }
+}
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html
new file mode 100644
index 000000000..cbcacd676
--- /dev/null
+++ b/src/app/components/shared-files/shared-files.component.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/shared-files/shared-files.component.spec.ts b/src/app/components/shared-files/shared-files.component.spec.ts
new file mode 100644
index 000000000..169788d30
--- /dev/null
+++ b/src/app/components/shared-files/shared-files.component.spec.ts
@@ -0,0 +1,161 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Router } from '@angular/router';
+import { TestBed, async, fakeAsync, tick } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { AlfrescoApiService } from 'ng2-alfresco-core';
+
+import { CommonModule } from '../../common/common.module';
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { SharedFilesComponent } from './shared-files.component';
+
+describe('SharedFilesComponent', () => {
+ let fixture;
+ let component: SharedFilesComponent;
+ let contentService: ContentManagementService;
+ let nodeService;
+ let alfrescoApi: AlfrescoApiService;
+ let router: Router;
+ let page;
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
+ pagination: { data: 'data'}
+ }
+ };
+ });
+
+ beforeEach(async(() => {
+ TestBed
+ .configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ SharedFilesComponent
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(SharedFilesComponent);
+ component = fixture.componentInstance;
+
+ contentService = TestBed.get(ContentManagementService);
+ alfrescoApi = TestBed.get(AlfrescoApiService);
+ nodeService = alfrescoApi.getInstance().nodes;
+ router = TestBed.get(Router);
+ });
+
+ }));
+
+ beforeEach(() => {
+ spyOn(alfrescoApi.sharedLinksApi, 'findSharedLinks').and.returnValue(Promise.resolve(page));
+ });
+
+ describe('OnInit', () => {
+ beforeEach(() => {
+ spyOn(component, 'refresh').and.callFake(val => val);
+ });
+
+ it('should refresh on deleteNode event', () => {
+ fixture.detectChanges();
+
+ contentService.deleteNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should refresh on restoreNode event', () => {
+ fixture.detectChanges();
+
+ contentService.restoreNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should refresh on favorite toggle event', () => {
+ fixture.detectChanges();
+
+ contentService.toggleFavorite.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+
+ it('should reload on move node event', () => {
+ fixture.detectChanges();
+
+ contentService.moveNode.next();
+
+ expect(component.refresh).toHaveBeenCalled();
+ });
+ });
+
+ describe('onNodeDoubleClick()', () => {
+ beforeEach(() => {
+ spyOn(component, 'fetchNodes').and.callFake(val => val);
+ fixture.detectChanges();
+ });
+
+ it('opens viewer if node is file', fakeAsync(() => {
+ spyOn(router, 'navigate').and.stub();
+ const link = { nodeId: 'nodeId' };
+ const node = { entry: { isFile: true, id: 'nodeId' } };
+
+ spyOn(nodeService, 'getNode').and.returnValue(Promise.resolve(node));
+ component.onNodeDoubleClick(link);
+ tick();
+
+ expect(router.navigate).toHaveBeenCalledWith(['/preview', node.entry.id]);
+ }));
+
+ it('does nothing if node is folder', fakeAsync(() => {
+ spyOn(router, 'navigate').and.stub();
+ spyOn(nodeService, 'getNode').and.returnValue(Promise.resolve({ entry: { isFile: false } }));
+ const link = { nodeId: 'nodeId' };
+
+ component.onNodeDoubleClick(link);
+ tick();
+
+ expect(router.navigate).not.toHaveBeenCalled();
+ }));
+
+ it('does nothing if link data is not passed', () => {
+ spyOn(router, 'navigate').and.stub();
+ spyOn(nodeService, 'getNode').and.returnValue(Promise.resolve({ entry: { isFile: true } }));
+
+ component.onNodeDoubleClick(null);
+
+ expect(router.navigate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('refresh', () => {
+ it('should call document list reload', () => {
+ spyOn(component.documentList, 'reload');
+ fixture.detectChanges();
+
+ component.refresh();
+
+ expect(component.documentList.reload).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/shared-files/shared-files.component.ts b/src/app/components/shared-files/shared-files.component.ts
new file mode 100644
index 000000000..90410e00c
--- /dev/null
+++ b/src/app/components/shared-files/shared-files.component.ts
@@ -0,0 +1,88 @@
+/*!
+ * @license
+ * Copyright 2017 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, OnInit, ViewChild, OnDestroy } from '@angular/core';
+import { Router } from '@angular/router';
+import { Subscription } from 'rxjs/Rx';
+import { MinimalNodeEntity } from 'alfresco-js-api';
+import { AlfrescoApiService } from 'ng2-alfresco-core';
+import { DocumentListComponent } from 'ng2-alfresco-documentlist';
+
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { PageComponent } from '../page.component';
+
+@Component({
+ templateUrl: './shared-files.component.html'
+})
+export class SharedFilesComponent extends PageComponent implements OnInit, OnDestroy {
+
+ @ViewChild(DocumentListComponent)
+ documentList: DocumentListComponent;
+
+ private onDeleteNode: Subscription;
+ private onMoveNode: Subscription;
+ private onRestoreNode: Subscription;
+ private onToggleFavorite: Subscription;
+
+ constructor(
+ private router: Router,
+ private content: ContentManagementService,
+ private apiService: AlfrescoApiService) {
+ super();
+ }
+
+ ngOnInit() {
+ this.onDeleteNode = this.content.deleteNode.subscribe(() => this.refresh());
+ this.onMoveNode = this.content.moveNode.subscribe(() => this.refresh());
+ this.onRestoreNode = this.content.restoreNode.subscribe(() => this.refresh());
+ this.onToggleFavorite = this.content.toggleFavorite.subscribe(() => this.refresh());
+ }
+
+ ngOnDestroy() {
+ this.onDeleteNode.unsubscribe();
+ this.onMoveNode.unsubscribe();
+ this.onRestoreNode.unsubscribe();
+ this.onToggleFavorite.unsubscribe();
+ }
+
+ onNodeDoubleClick(link: { nodeId?: string }) {
+ if (link && link.nodeId) {
+ this.apiService.nodesApi.getNode(link.nodeId).then(
+ (node: MinimalNodeEntity) => {
+ if (node && node.entry && node.entry.isFile) {
+ this.router.navigate(['/preview', node.entry.id]);
+ }
+ }
+ );
+ }
+ }
+
+ fetchNodes(parentNodeId?: string) {
+ // todo: remove once all views migrate to native data source
+ }
+
+ /** @override */
+ isFileSelected(selection: Array): boolean {
+ return selection && selection.length === 1;
+ }
+
+ refresh(): void {
+ if (this.documentList) {
+ this.documentList.reload();
+ }
+ }
+}
diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html
new file mode 100644
index 000000000..f19dab525
--- /dev/null
+++ b/src/app/components/sidenav/sidenav.component.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/sidenav/sidenav.component.scss b/src/app/components/sidenav/sidenav.component.scss
new file mode 100644
index 000000000..3a6bb0630
--- /dev/null
+++ b/src/app/components/sidenav/sidenav.component.scss
@@ -0,0 +1,78 @@
+@import '../../ui/_variables.scss';
+
+$sidenav-section--h-padding: 24px;
+$sidenav-section--v-padding: 8px;
+
+$sidenav-menu-item--h-padding: 24px;
+$sidenav-menu-item--v-padding: 12px;
+
+$sidenav-menu-item--icon-size: 24px;
+
+:host {
+ .sidenav {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+
+ &__section {
+ padding:
+ $sidenav-section--v-padding
+ $sidenav-section--h-padding;
+
+ border-bottom: 1px solid $alfresco-divider-color;
+ position: relative;
+
+ &--new {
+ padding-top: 2 * $sidenav-section--v-padding;
+ padding-bottom: 2 * $sidenav-section--v-padding;
+ }
+
+ &--new__button {
+ width: 100%;
+ color: $alfresco-white;
+ background-color: $alfresco-primary-accent--default;
+ }
+ }
+
+ &-menu {
+ margin: 0 -1 * $sidenav-section--h-padding;
+ padding: 0;
+ list-style-type: none;
+
+ &__item {
+ &-link {
+ padding:
+ $sidenav-menu-item--v-padding
+ $sidenav-menu-item--h-padding;
+
+ padding-left: $sidenav-menu-item--h-padding + 16px + 24px;
+ position: relative;
+ display: block;
+ color: $alfresco-secondary-text-color;
+ text-decoration: none;
+
+ & > .material-icons {
+ position: absolute;
+ top: 50%;
+ left: $sidenav-menu-item--h-padding;
+ margin-top: -14px;
+ }
+
+ &--active {
+ color: $alfresco-primary-accent--default;
+ }
+
+ &:not(&--active):hover {
+ color: $alfresco-primary-text-color;
+ }
+
+ &.disabled {
+ cursor: default !important;
+ color: $alfresco-secondary-text-color !important;
+ opacity: .25;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/components/sidenav/sidenav.component.spec.ts b/src/app/components/sidenav/sidenav.component.spec.ts
new file mode 100644
index 000000000..048df2247
--- /dev/null
+++ b/src/app/components/sidenav/sidenav.component.spec.ts
@@ -0,0 +1,85 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, async } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AlfrescoContentService } from 'ng2-alfresco-core';
+
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+
+import { SidenavComponent } from './sidenav.component';
+import { CommonModule } from './../../common/common.module';
+
+describe('SidenavComponent', () => {
+ let fixture;
+ let component: SidenavComponent;
+ let contentService: AlfrescoContentService;
+ let browsingService: BrowsingFilesService;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ CommonModule
+ ],
+ declarations: [
+ SidenavComponent
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ contentService = TestBed.get(AlfrescoContentService);
+ browsingService = TestBed.get(BrowsingFilesService);
+
+ fixture = TestBed.createComponent(SidenavComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+ }));
+
+ it('updates node on change', () => {
+ const node = { entry: { id: 'someNodeId' } };
+
+ browsingService.onChangeParent.next(node);
+
+ expect(component.node).toBe(node);
+ });
+
+ it('can create content', () => {
+ spyOn(contentService, 'hasPermission').and.returnValue(true);
+ const node: any = {};
+
+ expect(component.canCreateContent(node)).toBe(true);
+ expect(contentService.hasPermission).toHaveBeenCalledWith(node, 'create');
+ });
+
+ it('cannot create content for missing node', () => {
+ spyOn(contentService, 'hasPermission').and.returnValue(true);
+
+ expect(component.canCreateContent(null)).toBe(false);
+ expect(contentService.hasPermission).not.toHaveBeenCalled();
+ });
+
+ it('cannot create content based on permission', () => {
+ spyOn(contentService, 'hasPermission').and.returnValue(false);
+ const node: any = {};
+
+ expect(component.canCreateContent(node)).toBe(false);
+ expect(contentService.hasPermission).toHaveBeenCalledWith(node, 'create');
+ });
+});
diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts
new file mode 100644
index 000000000..6f61c9151
--- /dev/null
+++ b/src/app/components/sidenav/sidenav.component.ts
@@ -0,0 +1,105 @@
+/*!
+ * @license
+ * Copyright 2017 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 { Subscription } from 'rxjs/Rx';
+
+import { Component, OnInit, OnDestroy } from '@angular/core';
+
+import { MinimalNodeEntryEntity } from 'alfresco-js-api';
+import { AlfrescoContentService } from 'ng2-alfresco-core';
+
+import { BrowsingFilesService } from '../../common/services/browsing-files.service';
+
+@Component({
+ selector: 'app-sidenav',
+ templateUrl: './sidenav.component.html',
+ styleUrls: ['./sidenav.component.scss']
+})
+export class SidenavComponent implements OnInit, OnDestroy {
+ node: MinimalNodeEntryEntity = null;
+
+ onChangeParentSubscription: Subscription;
+
+ constructor(
+ private browsingFilesService: BrowsingFilesService,
+ private contentService: AlfrescoContentService
+ ) {}
+
+ get menus() {
+ const main = [
+ {
+ icon: 'folder',
+ label: 'APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/personal-files' }
+ },
+ {
+ icon: 'group_work',
+ label: 'APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/libraries' }
+ }
+ ];
+
+ const secondary = [
+ {
+ icon: 'people',
+ label: 'APP.BROWSE.SHARED.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/shared' }
+ },
+ {
+ icon: 'schedule',
+ label: 'APP.BROWSE.RECENT.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/recent-files' }
+ },
+ {
+ icon: 'star',
+ label: 'APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/favorites' }
+ },
+ {
+ icon: 'delete',
+ label: 'APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL',
+ title: 'APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP',
+ route: { url: '/trashcan' }
+ }
+ ];
+
+ return [ main, secondary ];
+ }
+
+ ngOnInit() {
+ this.onChangeParentSubscription = this.browsingFilesService.onChangeParent
+ .subscribe((node: MinimalNodeEntryEntity) => {
+ this.node = node;
+ });
+ }
+
+ ngOnDestroy() {
+ this.onChangeParentSubscription.unsubscribe();
+ }
+
+ canCreateContent(parentNode: MinimalNodeEntryEntity): boolean {
+ if (parentNode) {
+ return this.contentService.hasPermission(parentNode, 'create');
+ }
+ return false;
+ }
+}
diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html
new file mode 100644
index 000000000..77e4a2da9
--- /dev/null
+++ b/src/app/components/trashcan/trashcan.component.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+ {{ (value || '').split('/').pop() }}
+
+
+
+
+
+ {{ value | adfFileSize }}
+
+
+
+
+
+ {{ value | adfTimeAgo }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/trashcan/trashcan.component.spec.ts b/src/app/components/trashcan/trashcan.component.spec.ts
new file mode 100644
index 000000000..e5c8cdc96
--- /dev/null
+++ b/src/app/components/trashcan/trashcan.component.spec.ts
@@ -0,0 +1,77 @@
+/*!
+ * @license
+ * Copyright 2017 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 { TestBed, async } from '@angular/core/testing';
+import { CoreModule, AlfrescoApiService } from 'ng2-alfresco-core';
+import { TrashcanComponent } from './trashcan.component';
+import { CommonModule } from '../../common/common.module';
+
+describe('TrashcanComponent', () => {
+ let fixture;
+ let component;
+ let alfrescoApi: AlfrescoApiService;
+ let page;
+
+ beforeEach(() => {
+ page = {
+ list: {
+ entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
+ pagination: { data: 'data'}
+ }
+ };
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ CommonModule
+ ],
+ declarations: [
+ TrashcanComponent
+ ]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(TrashcanComponent);
+ component = fixture.componentInstance;
+
+ alfrescoApi = TestBed.get(AlfrescoApiService);
+
+ component.documentList = {
+ loadTrashcan: jasmine.createSpy('loadTrashcan'),
+ resetSelection: jasmine.createSpy('resetSelection')
+ };
+ });
+ }));
+
+ beforeEach(() => {
+ spyOn(alfrescoApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve(page));
+ });
+
+ describe('refresh()', () => {
+ it('calls child component to reload', () => {
+ component.refresh();
+ expect(component.documentList.loadTrashcan).toHaveBeenCalled();
+ });
+
+ it('calls child component to reset selection', () => {
+ component.refresh();
+ expect(component.documentList.resetSelection).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/components/trashcan/trashcan.component.ts b/src/app/components/trashcan/trashcan.component.ts
new file mode 100644
index 000000000..cb6575b2e
--- /dev/null
+++ b/src/app/components/trashcan/trashcan.component.ts
@@ -0,0 +1,31 @@
+/*!
+ * @license
+ * Copyright 2017 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, ViewChild } from '@angular/core';
+import { DocumentListComponent } from 'ng2-alfresco-documentlist';
+
+@Component({
+ templateUrl: './trashcan.component.html'
+})
+export class TrashcanComponent {
+ @ViewChild(DocumentListComponent) documentList;
+
+ refresh(): void {
+ this.documentList.loadTrashcan();
+ this.documentList.resetSelection();
+ }
+}
diff --git a/src/app/ui/_layout.scss b/src/app/ui/_layout.scss
new file mode 100644
index 000000000..e72873f94
--- /dev/null
+++ b/src/app/ui/_layout.scss
@@ -0,0 +1,68 @@
+@import './_variables.scss';
+
+$app-layout--header-height: 65px;
+$app-layout--side-width: 320px;
+
+$app-inner-layout--header-height: 48px;
+$app-inner-layout--footer-height: 48px;
+
+.layout {
+ display: flex;
+ flex-direction: column;
+ flex: 1 0;
+ overflow: hidden;
+
+ &__header {
+ flex: 0 0 $app-layout--header-height;
+ }
+
+ &__content {
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ overflow: hidden;
+
+ & > * {
+ display: flex;
+ flex: 1;
+ }
+
+ &-side {
+ flex: 0 0 $app-layout--side-width;
+ background: $alfresco-gray-background;
+ border-right: 1px solid $alfresco-divider-color;
+ }
+ }
+}
+
+.inner-layout {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+
+ &__header,
+ &__footer {
+ display: flex;
+ flex: 0 0;
+ align-items: center;
+ }
+
+ &__header {
+ flex-basis: $app-inner-layout--header-height;
+ background: $alfresco-gray-background;
+ border-bottom: 1px solid $alfresco-divider-color;
+ padding: 0 24px;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: hidden;
+ }
+
+ &__footer {
+ flex-basis: $app-inner-layout--footer-height;
+ border-top: 1px solid $alfresco-divider-color;
+ }
+}
diff --git a/src/app/ui/_variables-color.scss b/src/app/ui/_variables-color.scss
new file mode 100644
index 000000000..b993b6936
--- /dev/null
+++ b/src/app/ui/_variables-color.scss
@@ -0,0 +1,48 @@
+// Primary color palette
+// - please note that Hue 2 and Enhanced Hue 1 and 2
+// are missing from specs
+$alfresco-app-color--default: #00bcd4;
+$alfresco-app-color--hue-1: #e0f7fa;
+$alfresco-app-color--hue-3: #0097a7;
+
+// Primary color palette - Enhanced
+$alfresco-app-color--default-enhanced: #0097a7;
+$alfresco-app-color--hue-3-enhanced: #006064;
+
+// Accent color palette
+$alfresco-primary-accent--default: #ff9100;
+$alfresco-primary-accent--hue-1: #ffd180;
+$alfresco-primary-accent--hue-2: #ffab40;
+$alfresco-primary-accent--hue-3: #ff6d00;
+
+$alfresco-secondary-accent--default: #3d5afe;
+$alfresco-secondary-accent--hue-1: #8c9eff;
+$alfresco-secondary-accent--hue-2: #536dfe;
+$alfresco-secondary-accent--hue-3: #304ffe;
+
+// Warn color palette
+$alfresco-warn-color--default: #ff1744;
+$alfresco-warn-color--hue-1: #ff8a80;
+$alfresco-warn-color--hue-2: #ff5252;
+$alfresco-warn-color--hue-3: #d50000;
+
+// Grayscale
+$alfresco-white: #fff;
+$alfresco-black: #000;
+
+// Dark
+$alfresco-dark-color--default: #78909c;
+$alfresco-dark-color--hue-1: #eceff1;
+$alfresco-dark-color--hue-3: #546e7a;
+
+$alfresco-drop-shadow: #888888;
+
+$alfresco-primary-text-color: rgba($alfresco-black, .87);
+$alfresco-secondary-text-color: rgba($alfresco-black, .54);
+
+$alfresco-hint-text-color: rgba($alfresco-black, .38);
+$alfresco-disabled-text-color: rgba($alfresco-black, .26);
+
+$alfresco-divider-color: rgba($alfresco-black, .07);
+
+$alfresco-gray-background: #fafafa;
diff --git a/src/app/ui/_variables.scss b/src/app/ui/_variables.scss
new file mode 100644
index 000000000..5f127abb4
--- /dev/null
+++ b/src/app/ui/_variables.scss
@@ -0,0 +1,3 @@
+@import './_variables-color.scss';
+
+$app-menu-height: 64px;
diff --git a/src/app/ui/application.scss b/src/app/ui/application.scss
new file mode 100644
index 000000000..58c2fcae1
--- /dev/null
+++ b/src/app/ui/application.scss
@@ -0,0 +1,28 @@
+@import 'variables';
+@import 'theme';
+
+html, body {
+ display: flex;
+ font-size: 14px;
+ font-family: "Muli", sans-serif;
+ color: $alfresco-primary-text-color;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+alfresco-content-app > ng-component {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+@import 'layout';
+
+@import './overrides/alfresco-document-list';
+@import './overrides/alfresco-upload-drag-area';
+@import './overrides/alfresco-upload-button';
+@import './overrides/alfresco-upload-dialog';
+@import './overrides/toolbar';
+@import './overrides/breadcrumb';
diff --git a/src/app/ui/overrides/_alfresco-document-list.scss b/src/app/ui/overrides/_alfresco-document-list.scss
new file mode 100644
index 000000000..cce1777e5
--- /dev/null
+++ b/src/app/ui/overrides/_alfresco-document-list.scss
@@ -0,0 +1,83 @@
+@import '../_variables.scss';
+
+adf-document-list {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: auto;
+
+ & > adf-datatable {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: auto;
+ }
+}
+
+adf-document-list .adf-data-table {
+ border: none !important;
+
+ .sr-only {
+ display: none;
+ }
+
+ tr, td {
+ &:focus {
+ outline: none !important;
+ }
+ }
+
+ // TODO: Remove tr background-color once it gets to ADF
+ tr {
+ &:hover, &:focus {
+ background-color: $alfresco-app-color--hue-1;
+ }
+
+ &.is-selected {
+ background-color: $alfresco-app-color--hue-1;
+
+ &:hover {
+ background-color: $alfresco-app-color--hue-1;
+ }
+
+ .image-table-cell {
+ position: relative;
+
+ &:before {
+ content: "\E876"; /* "done" */
+ font-family: "Material Icons";
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+ color: white;
+ position: absolute;
+ width: 32px;
+ height: 32px;
+ top: 50%;
+ left: 50%;
+ margin-top: -16px;
+ margin-left: -14px;
+ border-radius: 100%;
+ background: #00bcd4;
+ }
+ }
+ }
+
+ .app-name-column {
+ width: 100%;
+
+ .cell-container {
+ max-width: 45vw;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ }
+
+ .adf-location-cell {
+ a {
+ text-decoration: none;
+ color: $alfresco-primary-text-color;
+ }
+ }
+}
diff --git a/src/app/ui/overrides/_alfresco-upload-button.scss b/src/app/ui/overrides/_alfresco-upload-button.scss
new file mode 100644
index 000000000..19bce1e62
--- /dev/null
+++ b/src/app/ui/overrides/_alfresco-upload-button.scss
@@ -0,0 +1,44 @@
+@import '../_variables.scss';
+
+alfresco-upload-button {
+ .mat-raised-button.mat-primary {
+ width: 100%;
+ border-radius: 0;
+ text-align: left;
+ line-height: 48px;
+ box-shadow: none;
+ transform: none;
+ transition: unset;
+ background-color: $alfresco-white;
+ }
+
+ .mat-raised-button.mat-primary:hover:not([disabled]) {
+ background-color: rgba(0, 0, 0, 0.04);
+ }
+
+ .mat-raised-button.mat-primary[disabled] {
+ background: none;
+ }
+
+ .mat-raised-button.mat-primary[disabled] label {
+ color: rgba(0, 0, 0, 0.38);
+ }
+
+ .mat-raised-button:not([disabled]):active {
+ box-shadow: none;
+ }
+
+ md-icon {
+ color: rgba(0, 0, 0, 0.54);
+ }
+
+ label {
+ text-transform: capitalize;
+ font-family: Muli;
+ font-size: 16px;
+ font-weight: normal;
+ text-align: left;
+ margin-left: 18px;
+ color: $alfresco-primary-text-color;
+ }
+}
diff --git a/src/app/ui/overrides/_alfresco-upload-dialog.scss b/src/app/ui/overrides/_alfresco-upload-dialog.scss
new file mode 100644
index 000000000..85970710c
--- /dev/null
+++ b/src/app/ui/overrides/_alfresco-upload-dialog.scss
@@ -0,0 +1,23 @@
+@import '../_variables.scss';
+
+.adf-file-uploading-row {
+ &__status {
+ &--done {
+ color: $alfresco-app-color--default !important;
+ }
+
+ &--error {
+ color: $alfresco-primary-accent--hue-3 !important;
+ }
+ }
+
+ &__action {
+ &--cancel {
+ color: $alfresco-warn-color--hue-3 !important;
+ }
+
+ &--remove {
+ color: $alfresco-dark-color--hue-3 !important;
+ }
+ }
+}
diff --git a/src/app/ui/overrides/_alfresco-upload-drag-area.scss b/src/app/ui/overrides/_alfresco-upload-drag-area.scss
new file mode 100644
index 000000000..51138cc0b
--- /dev/null
+++ b/src/app/ui/overrides/_alfresco-upload-drag-area.scss
@@ -0,0 +1,72 @@
+@import '../_variables.scss';
+
+@mixin file-draggable__input-focus {
+ color: $alfresco-secondary-text-color !important;
+ border: 1px solid $alfresco-app-color--default !important;
+ margin-left: 0 !important;
+}
+
+alfresco-upload-drag-area:first-child {
+ & > div {
+ alfresco-upload-drag-area {
+ .file-draggable__input-focus {
+ @include file-draggable__input-focus;
+ }
+ }
+ }
+
+ .upload-border {
+ vertical-align: inherit !important;
+ text-align: inherit !important;
+ }
+
+ .file-draggable__input-focus {
+ color: none !important;
+ border: none !important;
+ margin-left: 0 !important;
+
+ alfresco-upload-drag-area {
+ & > div {
+ @include file-draggable__input-focus;
+ }
+ }
+ }
+}
+
+alfresco-upload-drag-area {
+ height: 100%;
+
+ & > div {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .file-draggable__input-focus {
+ alfresco-document-list {
+ background: $alfresco-app-color--hue-1;
+
+ alfresco-datatable > table {
+ background: inherit;
+ }
+ }
+ }
+
+ .adf-upload__dragging {
+ background: $alfresco-app-color--hue-1;
+ color: $alfresco-secondary-text-color !important;
+ }
+
+ .adf-upload__dragging td {
+ border-top: 1px solid $alfresco-app-color--default !important;
+ border-bottom: 1px solid $alfresco-app-color--default !important;
+
+ &:first-child {
+ border-left: 1px solid $alfresco-app-color--default !important;
+ }
+
+ &:last-child {
+ border-right: 1px solid $alfresco-app-color--default !important;
+ }
+ }
+}
diff --git a/src/app/ui/overrides/_breadcrumb.scss b/src/app/ui/overrides/_breadcrumb.scss
new file mode 100644
index 000000000..0e33f840c
--- /dev/null
+++ b/src/app/ui/overrides/_breadcrumb.scss
@@ -0,0 +1,5 @@
+@import '../variables';
+
+.adf-breadcrumb {
+ width: 0;
+}
diff --git a/src/app/ui/overrides/_toolbar.scss b/src/app/ui/overrides/_toolbar.scss
new file mode 100644
index 000000000..65bf17fc0
--- /dev/null
+++ b/src/app/ui/overrides/_toolbar.scss
@@ -0,0 +1,36 @@
+@import 'variables.scss';
+
+.adf-toolbar {
+ // TODO: review and remove once ADF 2.0.0 is out
+ &.inline {
+ .mat-toolbar {
+ border-left: none !important;
+ border-right: none !important;
+ background: $alfresco-gray-background;
+ padding: 0;
+ height: 48px;
+ }
+
+ // TODO remove once it gets to ADF
+ .mat-icon {
+ color: $alfresco-secondary-text-color;
+ }
+
+ &-row {
+ & > button {
+ color: $alfresco-secondary-text-color;
+ }
+ }
+ }
+}
+
+.secondary-options {
+ // TODO remove once it gets to ADF
+ button {
+ color: $alfresco-secondary-text-color;
+ }
+
+ .icon-highlight {
+ color: $alfresco-primary-accent--default;
+ }
+}
diff --git a/src/app/ui/theme.scss b/src/app/ui/theme.scss
new file mode 100644
index 000000000..4f7edb0ae
--- /dev/null
+++ b/src/app/ui/theme.scss
@@ -0,0 +1,25 @@
+@import "~@angular/material/theming";
+
+@import '~ng2-alfresco-core/styles/theming';
+@import '~ng2-alfresco-core/styles/index';
+@import '~ng2-alfresco-datatable/styles/index';
+@import '~ng2-alfresco-documentlist/styles/index';
+@import '~ng2-alfresco-login/styles/index';
+@import '~ng2-alfresco-upload/styles/index';
+@import '~ng2-alfresco-search/styles/index';
+
+@include mat-core();
+
+$primary: mat-palette($alfresco-accent-orange);
+$accent: mat-palette($alfresco-ecm-blue);
+$warn: mat-palette($alfresco-warn);
+$theme: mat-light-theme($primary, $accent, $warn);
+
+@include angular-material-theme($theme);
+
+@include alfresco-core-theme($theme);
+@include alfresco-datatable-theme($theme);
+@include alfresco-documentlist-theme($theme);
+@include alfresco-login-theme($theme);
+@include alfresco-upload-theme($theme);
+@include alfresco-search-theme($theme);
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
new file mode 100644
index 000000000..255a3ba6b
--- /dev/null
+++ b/src/assets/i18n/en.json
@@ -0,0 +1,201 @@
+{
+ "APP": {
+ "SIGN_IN": "Sign in",
+ "SIGN_OUT": "Sign out",
+ "NEW_MENU": {
+ "LABEL": "New",
+ "MENU_ITEMS": {
+ "CREATE_FOLDER": "Create folder",
+ "UPLOAD_FILE": "Upload file",
+ "UPLOAD_FOLDER": "Upload folder"
+ },
+ "TOOLTIPS": {
+ "CREATE_FOLDER": "Create new folder",
+ "CREATE_FOLDER_NOT_ALLOWED": "You can't create a folder here. You might not have the required permissions, check with your IT Team.",
+ "UPLOAD_FILES": "Select files to upload",
+ "UPLOAD_FILES_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team.",
+ "UPLOAD_FOLDERS": "Select folders to upload",
+ "UPLOAD_FOLDERS_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team."
+ }
+ },
+ "BROWSE": {
+ "PERSONAL": {
+ "TITLE": "Personal Files",
+ "SIDENAV_LINK": {
+ "LABEL": "Personal Files",
+ "TOOLTIP": "View your Personal Files"
+ }
+ },
+ "LIBRARIES": {
+ "TITLE": "File Libraries",
+ "SIDENAV_LINK": {
+ "LABEL": "File Libraries",
+ "TOOLTIP": "Access File Libraries"
+ }
+ },
+ "SHARED": {
+ "TITLE": "Shared Files",
+ "SIDENAV_LINK": {
+ "LABEL": "Shared",
+ "TOOLTIP": "View files that have been shared"
+ }
+ },
+ "RECENT": {
+ "TITLE": "Recent Files",
+ "SIDENAV_LINK": {
+ "LABEL": "Recent Files",
+ "TOOLTIP": "View files you recently edited"
+ }
+ },
+ "FAVORITES": {
+ "TITLE": "Favorites",
+ "SIDENAV_LINK": {
+ "LABEL": "Favorites",
+ "TOOLTIP": "View your favorite files and folders"
+ }
+ },
+ "TRASHCAN": {
+ "TITLE": "Trash",
+ "SIDENAV_LINK": {
+ "LABEL": "Trash",
+ "TOOLTIP": "View deleted files in the trash"
+ }
+ }
+ },
+ "ACTIONS": {
+ "VIEW": "View",
+ "EDIT": "Edit",
+ "DOWNLOAD": "Download",
+ "COPY": "Copy",
+ "MOVE": "Move",
+ "DELETE": "Delete",
+ "MORE": "More actions",
+ "UNDO": "Undo",
+ "RESTORE": "Restore",
+ "FAVORITE": "Favorite"
+ },
+ "DOCUMENT_LIST": {
+ "COLUMNS": {
+ "NAME": "Name",
+ "SIZE": "Size",
+ "MODIFIED_ON": "Modified",
+ "MODIFIED_BY": "Modified by",
+ "STATUS": "Status",
+ "TITLE": "Title",
+ "LOCATION": "Location",
+ "SHARED_BY": "Shared by",
+ "DELETED_ON": "Deleted",
+ "DELETED_BY": "Deleted by"
+ }
+ },
+ "DIALOG": {
+ "MINIMIZE": "Minimize",
+ "MAXIMIZE": "Maximize",
+ "CLOSE": "Close",
+ "UPLOAD": {
+ "IN_PROGRESS": "Uploading {{ completed }} / {{ length }}",
+ "COMPLETE": "Uploaded {{ completed }} / {{ length }}",
+ "CANCELED": "Upload canceled",
+ "CANCEL_UPLOAD": "Cancel upload",
+ "CANCEL_ALL": "Cancel uploads",
+ "REMOVE_UPLOADED": "Remove uploaded file",
+ "CANCELED_STATUS": "Canceled",
+ "ERRORS": "error(s)"
+ }
+ },
+ "FOLDER_DIALOG": {
+ "CREATE_FOLDER_TITLE": "Create new folder",
+ "EDIT_FOLDER_TITLE": "Edit folder",
+ "FOLDER_NAME": {
+ "LABEL": "Name",
+ "ERRORS": {
+ "REQUIRED": "Folder name is required",
+ "SPECIAL_CHARACTERS": "Folder name can't contain these characters * \" < > \\ / ? : |",
+ "ENDING_DOT": "Folder name can't end with a period .",
+ "ONLY_SPACES": "Folder name can't contain only spaces"
+ }
+ },
+ "FOLDER_DESCRIPTION": {
+ "LABEL": "Description"
+ },
+ "CREATE_BUTTON": {
+ "LABEL": "Create"
+ },
+ "UPDATE_BUTTON": {
+ "LABEL": "Update"
+ },
+ "CANCEL_BUTTON": {
+ "LABEL": "Cancel"
+ }
+ },
+ "PAGINATION": {
+ "ITEMS_PER_PAGE": "Items per page",
+ "CURRENT_PAGE": "Page",
+ "OF": "of",
+ "SHOW_RANGE": "Showing {{ range }} of {{ total }} items"
+ },
+ "SITES_VISIBILITY": {
+ "PUBLIC": "Public",
+ "MODERATED": "Moderated",
+ "PRIVATE": "Private"
+ },
+ "MESSAGES": {
+ "ERRORS":{
+ "GENERIC": "The action was unsuccessful. Try again or contact your IT Team.",
+ "CONFLICT": "This name is already in use, try a different name.",
+ "NODE_MOVE": "Move unsuccessful, a file with the same name already exists.",
+ "EXISTENT_FOLDER": "There's already a folder with this name. Try a different name.",
+ "NODE_DELETION": "{{ name }} couldn't be deleted",
+ "NODE_DELETION_PLURAL": "{{ number }} items couldn't be deleted",
+ "NODE_RESTORE": "{{ name }} couldn't be restored",
+ "NODE_RESTORE_PLURAL": "{{ number }} items couldn't be restored",
+ "PERMISSION": "You don't have access to do this",
+ "TRASH": {
+ "NODES_PURGE": {
+ "PLURAL": "{{ number }} items couldn't be deleted",
+ "SINGULAR": "{{ name }} item couldn't be deleted"
+ },
+ "NODES_RESTORE": {
+ "PARTIAL_PLURAL": "{{ number }} items not restored because of issues with the restore location",
+ "NODE_EXISTS": "Can't restore, {{ name }} item already exists",
+ "LOCATION_MISSING": "Can't restore {{ name }} item, the original location no longer exists",
+ "GENERIC": "There was a problem restoring {{ name }} item"
+ }
+ }
+ },
+ "INFO": {
+ "TRASH": {
+ "NODES_PURGE": {
+ "PLURAL": "{{ number }} items deleted",
+ "SINGULAR": "{{ name }} item deleted",
+ "PARTIAL_SINGULAR": "{{ name }} item deleted, {{ failed }} couldn't be deleted",
+ "PARTIAL_PLURAL": "{{ number }} items deleted, {{ failed }} couldn't be deleted"
+ },
+ "NODES_RESTORE": {
+ "PLURAL": "Restore successful",
+ "SINGULAR": "{{ name }} item restored"
+ }
+ },
+ "NODE_DELETION": {
+ "SINGULAR": "{{ name }} deleted",
+ "PLURAL": "Deleted {{ number }} items",
+ "PARTIAL_SINGULAR": "Deleted {{ success }} item, {{ failed }} couldn't be deleted",
+ "PARTIAL_PLURAL": "Deleted {{ success }} items, {{ failed }} couldn't be deleted"
+ },
+ "NODE_COPY": {
+ "SINGULAR": "Copied {{ number }} item",
+ "PLURAL": "Copied {{ number }} items"
+ },
+ "NODE_MOVE": {
+ "SINGULAR": "Moved {{ success }} item.",
+ "PLURAL": "Moved {{ success }} items.",
+ "PARTIAL": {
+ "SINGULAR": "Partially moved {{ partially }} item.",
+ "PLURAL": "Partially moved {{ partially }} items.",
+ "FAIL": "{{ failed }} couldn't be moved."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json
new file mode 100644
index 000000000..141d2cf52
--- /dev/null
+++ b/src/assets/i18n/ru.json
@@ -0,0 +1,125 @@
+{
+ "APP": {
+ "SIGN_IN": "russian Sign in",
+ "SIGN_OUT": "russian Sign out",
+ "NEW_MENU": {
+ "TITLE": "russian New",
+ "MENU_ITEMS": {
+ "UPLOAD_FILE": "russian Upload file",
+ "UPLOAD_FOLDER": "russian Upload folder"
+ }
+ },
+ "BROWSE": {
+ "PERSONAL": {
+ "TITLE": "Личные файлы",
+ "SIDENAV_LINK": {
+ "LABEL": "Личные файлы",
+ "TOOLTIP": "russian personal tooltip"
+ }
+ },
+ "LIBRARIES": {
+ "TITLE": "russian libraries title",
+ "SIDENAV_LINK": {
+ "LABEL": "russian libraries label",
+ "TOOLTIP": "russian libraries tooltip"
+ }
+ },
+ "SHARED": {
+ "TITLE": "russian shared title",
+ "SIDENAV_LINK": {
+ "LABEL": "russian shared label",
+ "TOOLTIP": "russian shared tooltip"
+ }
+ },
+ "RECENT": {
+ "TITLE": "Недавние файлы",
+ "SIDENAV_LINK": {
+ "LABEL": "Недавние файлы",
+ "TOOLTIP": "russian recent tooltip"
+ }
+ },
+ "FAVORITES": {
+ "TITLE": "russian favorites title",
+ "SIDENAV_LINK": {
+ "LABEL": "russian favorites label",
+ "TOOLTIP": "russian favorites tooltip"
+ }
+ },
+ "TRASHCAN": {
+ "TITLE": "russian trashcan title",
+ "SIDENAV_LINK": {
+ "LABEL": "russian trashcan label",
+ "TOOLTIP": "russian trashcan tooltip"
+ }
+ }
+ },
+ "ACTIONS": {
+ "VIEW": "Просмотр",
+ "SHARE": "Поделиться",
+ "DOWNLOAD": "Скачать",
+ "COPY": "Копировать",
+ "MOVE": "Переместить",
+ "DELETE": "Удалить",
+ "MORE": "russian More actions"
+ },
+ "DOCUMENT_LIST": {
+ "COLUMNS": {
+ "NAME": "russian Name",
+ "SIZE": "russian Size",
+ "MODIFIED_ON": "russian Modified",
+ "MODIFIED_BY": "russian Modified by",
+ "STATUS": "russian Status",
+ "TITLE": "russian Title",
+ "LOCATION": "russian Location",
+ "SHARED_BY": "russian Shared by",
+ "DELETED_ON": "russian Deleted",
+ "DELETED_BY": "russian Deleted by"
+ }
+ },
+ "DIALOG": {
+ "MINIMIZE": "russian Minimize",
+ "MAXIMIZE": "russian Maximize",
+ "CLOSE": "russian Close",
+ "UPLOAD": {
+ "IN_PROGRESS": "russian Uploading {{ completed }} / {{ length }}",
+ "CANCELED": "russian Upload canceled",
+ "CANCEL_UPLOAD": "russian Cancel upload",
+ "CANCEL_ALL": "russian Cancel uploads",
+ "REMOVE_UPLOADED": "russian Remove uploaded file",
+ "CANCELED_STATUS": "russian Canceled"
+ }
+ },
+ "PAGINATION": {
+ "ITEMS_PER_PAGE": "russian Items per page",
+ "CURRENT_PAGE": "russian Page",
+ "OF": "russian of",
+ "SHOW_RANGE": "russian Showing {{ range }} of {{ total }} items"
+ },
+ "PIPES": {
+ "DATE": {
+ "NOW": "russian just now",
+ "1_MINUTE_AGO": "russian 1 minute ago",
+ "MINUTES_AGO": "russian {{minutes }} minutes ago",
+ "1_HOUR_AGO": "russian 1 hour ago",
+ "HOURS_AGO": "russian {{ hours }} hours ago",
+ "1_DAY_AGO": "russian 1 day ago",
+ "DAYS_AGO": "russian {{ days }} days ago"
+ },
+ "SIZE": {
+ "UNITS": {
+ "BYTE": "russian B",
+ "KILOBYTE": "russian kB",
+ "MEGABYTE": "russian MB",
+ "GIGABYTE": "russian GB",
+ "TERABYTE": "russian TB"
+ },
+ "BYTES": "{{ bytes }} russian bytes"
+ }
+ },
+ "SITES_VISIBILITY": {
+ "PUBLIC": "russian Public",
+ "MODERATED": "russian Moderated",
+ "PRIVATE": "russian Private"
+ }
+ }
+}
diff --git a/src/assets/images/alfresco-logo-white.svg b/src/assets/images/alfresco-logo-white.svg
new file mode 100644
index 000000000..6ec673eb3
--- /dev/null
+++ b/src/assets/images/alfresco-logo-white.svg
@@ -0,0 +1,129 @@
+
+
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.scss
similarity index 91%
rename from src/styles.css
rename to src/styles.scss
index c4a5b46f9..d71979366 100644
--- a/src/styles.css
+++ b/src/styles.scss
@@ -1,6 +1,7 @@
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
@import '~ng2-alfresco-core/prebuilt-themes/adf-blue-orange.css';
+@import 'app/ui/application';
body, html {
height: 100%;