main application

This commit is contained in:
Denys Vuika
2017-10-19 11:21:51 +01:00
parent 8809c1e122
commit 1bf4f26df8
100 changed files with 11535 additions and 222 deletions

View File

@@ -30,7 +30,7 @@
"testTsconfig": "tsconfig.spec.json", "testTsconfig": "tsconfig.spec.json",
"prefix": "app", "prefix": "app",
"styles": [ "styles": [
"styles.css" "styles.scss"
], ],
"scripts": [ "scripts": [
"../node_modules/pdfjs-dist/build/pdf.js", "../node_modules/pdfjs-dist/build/pdf.js",
@@ -41,6 +41,11 @@
"environments": { "environments": {
"dev": "environments/environment.ts", "dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts" "prod": "environments/environment.prod.ts"
},
"stylePreprocessorOptions": {
"includePaths": [
"app/ui"
]
} }
} }
], ],

View File

@@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@@ -10,20 +10,19 @@ import { UploadModule } from 'ng2-alfresco-upload';
import { SearchModule } from 'ng2-alfresco-search'; import { SearchModule } from 'ng2-alfresco-search';
export function modules() { export function modules() {
return [ return [
// ADF modules CoreModule,
CoreModule, DataTableModule,
DataTableModule, DocumentListModule,
DocumentListModule, LoginModule,
LoginModule, SearchModule,
SearchModule, UploadModule,
UploadModule, ViewerModule
ViewerModule ];
];
} }
@NgModule({ @NgModule({
imports: modules(), imports: modules(),
exports: modules() exports: modules()
}) })
export class AdfModule {} export class AdfModule { }

View File

@@ -0,0 +1,4 @@
:host {
display: flex;
flex: 1;
}

View File

@@ -1,27 +1,27 @@
import { TestBed, async } from '@angular/core/testing'; import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ declarations: [
AppComponent AppComponent
], ],
}).compileComponents(); }).compileComponents();
})); }));
it('should create the app', async(() => { it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy(); expect(app).toBeTruthy();
})); }));
it(`should have as title 'app'`, async(() => { it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app'); expect(app.title).toEqual('app');
})); }));
it('should render title in a h1 tag', async(() => { it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement; const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
})); }));
}); });

View File

@@ -20,41 +20,41 @@ import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { TranslationService, PageTitleService } from 'ng2-alfresco-core'; import { TranslationService, PageTitleService } from 'ng2-alfresco-core';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private pageTitle: PageTitleService, private pageTitle: PageTitleService,
private translateService: TranslationService) { private translateService: TranslationService) {
} }
ngOnInit() { ngOnInit() {
const { router, pageTitle, route, translateService } = this; const { router, pageTitle, route, translateService } = this;
router router
.events .events
.filter(event => event instanceof NavigationEnd) .filter(event => event instanceof NavigationEnd)
.subscribe(() => { .subscribe(() => {
let currentRoute = route.root; let currentRoute = route.root;
while (currentRoute.firstChild) { while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild; currentRoute = currentRoute.firstChild;
} }
const snapshot: any = currentRoute.snapshot || {}; const snapshot: any = currentRoute.snapshot || {};
const data: any = snapshot.data || {}; const data: any = snapshot.data || {};
if (data.i18nTitle) { if (data.i18nTitle) {
translateService.get(data.i18nTitle).subscribe(title => { translateService.get(data.i18nTitle).subscribe(title => {
pageTitle.setTitle(title); pageTitle.setTitle(title);
}); });
} else { } else {
pageTitle.setTitle(data.title || ''); pageTitle.setTitle(data.title || '');
} }
}); });
} }
} }

View File

@@ -1,26 +1,65 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
import { AdfModule } from './adf.module'; import { AdfModule } from './adf.module';
import { CommonModule } from './common/common.module';
import { MaterialModule } from './common/material.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { APP_ROUTES } from './app.routes'; import { APP_ROUTES } from './app.routes';
import { LoginComponent } from './components/login/login.component'; 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({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
RouterModule.forRoot(APP_ROUTES, { RouterModule.forRoot(APP_ROUTES, {
enableTracing: true enableTracing: true
}), }),
AdfModule AdfModule,
], CommonModule,
declarations: [ MaterialModule
AppComponent, ],
LoginComponent declarations: [
], AppComponent,
providers: [], LoginComponent,
bootstrap: [AppComponent] 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 { } export class AppModule { }

View File

@@ -16,20 +16,111 @@
*/ */
import { Routes } from '@angular/router'; 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 { LoginComponent } from './components/login/login.component';
import { PreviewComponent } from './components/preview/preview.component';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
{ {
path: '**', path: '',
redirectTo: '/login', component: LayoutComponent,
pathMatch: 'full' children: [
}, {
{ path: '',
path: 'login', redirectTo: `/personal-files`,
component: LoginComponent, pathMatch: 'full'
data: { },
title: 'Sign in' {
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'
}
} }
}
]; ];

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -0,0 +1,62 @@
<h2 md-dialog-title>
{{
(editing
? 'APP.FOLDER_DIALOG.EDIT_FOLDER_TITLE'
: 'APP.FOLDER_DIALOG.CREATE_FOLDER_TITLE'
) | translate
}}
</h2>
<md-dialog-content>
<form [formGroup]="form" (submit)="submit()">
<md-input-container style="width: 100%">
<input
placeholder="{{ 'APP.FOLDER_DIALOG.FOLDER_NAME.LABEL' | translate }}"
mdInput
required
[formControl]="form.controls['name']"
/>
<md-hint *ngIf="form.controls['name'].dirty">
<span *ngIf="form.controls['name'].errors?.required">
{{ 'APP.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
</span>
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
{{ form.controls['name'].errors?.message | translate }}
</span>
</md-hint>
</md-input-container>
<br />
<br />
<md-input-container style="width: 100%">
<textarea
mdInput
placeholder="{{ 'APP.FOLDER_DIALOG.FOLDER_DESCRIPTION.LABEL' | translate }}"
rows="4"
[formControl]="form.controls['description']"></textarea>
</md-input-container>
</form>
</md-dialog-content>
<md-dialog-actions>
<button
md-raised-button
(click)="submit()"
[disabled]="!form.valid">
{{
(editing
? 'APP.FOLDER_DIALOG.UPDATE_BUTTON.LABEL'
: 'APP.FOLDER_DIALOG.CREATE_BUTTON.LABEL'
) | translate
}}
</button>
<button
md-button
md-dialog-close>
{{ 'APP.FOLDER_DIALOG.CANCEL_BUTTON.LABEL' | translate }}
</button>
</md-dialog-actions>

View File

@@ -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<FolderDialogComponent>;
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');
});
});
});

View File

@@ -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<FolderDialogComponent>,
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<MinimalNodeEntryEntity> {
const { name, properties, nodesApi, data: { parentNodeId} } = this;
return nodesApi.createFolder(parentNodeId, { name, properties });
}
private edit(): Observable<MinimalNodeEntryEntity> {
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;
}
}

View File

@@ -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`
};
}

View File

@@ -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: '<div [app-create-folder]="parentNode"></div>'
})
class TestComponent {
parentNode = '';
}
describe('FolderCreateDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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();
});
});

View File

@@ -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);
}
});
}
}

View File

@@ -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: '<div [app-edit-folder]="folder"></div>'
})
class TestComponent {
folder = {};
}
describe('FolderEditDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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();
});
});

View File

@@ -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);
}
});
}
}

View File

@@ -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: '<div [app-copy-node]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeCopyDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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(<any>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(<any>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(<any>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(<any>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(<any>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(<any>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(<any>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]]);
});
});
});

View File

@@ -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);
});
}
);
}
}

View File

@@ -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: '<div [app-delete-node]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeDeleteDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
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();
});
});
});

View File

@@ -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<any> {
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<string> {
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<string> {
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 }
);
}
}
}

View File

@@ -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: '<div [app-download-node]="selection"></div>'
})
class TestComponent {
selection;
}
describe('DownloadFileDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
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');
});
});

View File

@@ -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<MinimalNodeEntity>) {
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 && (<any>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<MinimalNodeEntity>) {
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);
}
}
}

View File

@@ -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: '<div [app-favorite-node]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeFavoriteDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
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);
});
});
});

View File

@@ -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-', <any>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<any> {
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);
}
}

View File

@@ -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: '<div [app-move-node]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeMoveDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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
);
});
});
});

View File

@@ -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);
});
}
);
}
}

View File

@@ -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: `<div [app-permanent-delete-node]="selection"></div>`
})
class TestComponent {
selection = [];
}
describe('NodePermanentDeleteDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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();
}));
});
});

View File

@@ -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<MinimalNodeEntity[]> {
return selection.map((node: MinimalNodeEntity) => this.purgeDeletedNode(node));
}
private purgeDeletedNode(node): Observable<any> {
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<string|any> {
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);
}
}

View File

@@ -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: `<div [app-restore-node]="selection"></div>`
})
class TestComponent {
selection = [];
}
describe('NodeRestoreDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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();
}));
});
});

View File

@@ -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<MinimalNodeEntity[]> {
return Observable.forkJoin(batch.map((node) => this.restoreNode(node)));
}
private getNodesWithPath(selection): MinimalNodeEntity[] {
return selection.filter((node) => node.entry.path);
}
private getDeletedNodes(): Observable<DeletedNodeEntry> {
const promise = this.alfrescoApiService.getInstance()
.core.nodesApi.getDeletedNodes({ include: [ 'path' ] });
return Observable.from(promise);
}
private restoreNode(node): Observable<any> {
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<string|any> {
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);
}
}

View File

@@ -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 {}

View File

@@ -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(<any> {})).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(<MinimalNodeEntity> 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);
});
});

View File

@@ -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`);
}
}

View File

@@ -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(<any>value);
});
});

View File

@@ -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<MinimalNodeEntryEntity>();
}

View File

@@ -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<MinimalNodeEntryEntity>();
editFolder = new Subject<MinimalNodeEntryEntity>();
deleteNode = new Subject<string>();
moveNode = new Subject<string>();
restoreNode = new Subject<string>();
toggleFavorite = new Subject<null>();
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<MinimalNodeEntity[]> = new Subject<MinimalNodeEntity[]>();
contentMoved: Subject<any> = new Subject<any>();
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<string> {
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<string> {
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<string> {
const observable: Subject<string> = new Subject<string>();
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<any>;
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<MinimalNodeEntryEntity[]> {
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<MinimalNodeEntryEntity[]>()
};
this.dialog.open(ContentNodeSelectorComponent, {
data,
panelClass: 'adf-content-node-selector-dialog',
width: '630px'
});
return data.select;
}
copyNodeAction(nodeEntry, selectionId): Observable<any> {
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<any> {
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<any> {
// Check if there's nodeId for Shared Files
const contentEntryId = contentEntry.nodeId || contentEntry.id;
let $destinationFolder: Observable<any>;
let $childrenToCopy: Observable<any>;
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<any> {
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<any> {
// Check if there's nodeId for Shared Files
const contentEntryId = contentEntry.nodeId || contentEntry.id;
const initialParentId = this.getEntryParentId(contentEntry);
let $destinationFolder: Observable<any>;
let $childrenToMove: Observable<any>;
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<any> = new Subject<any>();
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;
}
}
}

View File

@@ -0,0 +1,18 @@
<div>
<div title="{{ user?.id }}">
<span>
<span class="current-user__full-name">{{ userName }}</span>
<span
class="current-user__avatar am-avatar am-avatar--light"
[mdMenuTriggerFor]="userMenu">
{{ userInitials }}
</span>
</span>
</div>
<md-menu #userMenu="mdMenu" [overlapTrigger]="false">
<button md-menu-item adf-logout>
{{ 'APP.SIGN_OUT' | translate }}
</button>
</md-menu>
</div>

View File

@@ -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;
}
}
}

View File

@@ -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');
});
});

View File

@@ -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('');
}
}

View File

@@ -0,0 +1,135 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb root="APP.BROWSE.FAVORITES.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
md-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(documentList.selection[0]?.entry?.id)">
<md-icon>open_in_browser</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[app-download-node]="documentList.selection">
<md-icon>get_app</md-icon>
</button>
<button
md-icon-button
*ngIf="canEditFolder(documentList.selection)"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
[app-edit-folder]="documentList.selection[0]?.entry">
<md-icon>create</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[mdMenuTriggerFor]="actionsMenu">
<md-icon>more_vert</md-icon>
</button>
<md-menu #actionsMenu="mdMenu"
[overlapTrigger]="false"
class="secondary-options">
<button
md-menu-item
#favorite="favorite"
[app-favorite-node]="documentList.selection">
<md-icon [ngClass]="{ 'icon-highlight': favorite.hasFavorites() }">
{{ favorite.hasFavorites() ? 'star' :'star_border' }}
</md-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
md-menu-item
[app-copy-node]="documentList.selection">
<md-icon>content_copy</md-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<md-icon>library_books</md-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<md-icon>delete</md-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
</md-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
currentFolderId="-favorites-"
selectionMode="multiple"
[navigate]="false"
[sorting]="[ 'modifiedAt', 'desc' ]"
[pageSize]="25"
[contextMenuActions]="true"
[contentActions]="false"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="name"
class="app-name-column"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span title="{{ context?.row?.obj | nodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION"
type="location"
format="/personal-files">
</data-column>
<data-column
key="sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON"
type="date"
format="timeAgo">
</data-column>
<data-column
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
</div>
</div>

View File

@@ -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 = <any> {
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();
});
});
});

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,149 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb
[root]="title | translate"
[folderNode]="node"
(navigate)="onBreadcrumbNavigate($event)">
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
md-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(documentList.selection[0]?.entry?.id)">
<md-icon>open_in_browser</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[app-download-node]="documentList.selection">
<md-icon>get_app</md-icon>
</button>
<button
md-icon-button
*ngIf="canEditFolder(documentList.selection)"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
[app-edit-folder]="documentList.selection[0]?.entry">
<md-icon>create</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[mdMenuTriggerFor]="actionsMenu">
<md-icon>more_vert</md-icon>
</button>
<md-menu #actionsMenu="mdMenu"
[overlapTrigger]="false"
class="secondary-options">
<button
md-menu-item
#favorite="favorite"
[app-favorite-node]="documentList.selection">
<md-icon [ngClass]="{ 'icon-highlight': favorite.hasFavorites() }">
{{ favorite.hasFavorites() ? 'star' :'star_border' }}
</md-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
md-menu-item
[app-copy-node]="documentList.selection">
<md-icon>content_copy</md-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<md-icon>library_books</md-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<md-icon>delete</md-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
</md-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<alfresco-upload-drag-area
[rootFolderId]="node?.id"
[disabled]="!canCreateContent(node)"
[showNotificationBar]="false">
<adf-document-list #documentList
[loading]="isLoading"
[node]="paging"
[sorting]="[ 'modifiedAt', 'desc' ]"
[allowDropFiles]="true"
[contextMenuActions]="true"
[contentActions]="false"
[navigate]="false"
[enablePagination]="false"
[selectionMode]="'multiple'"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="name"
class="app-name-column"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span title="{{ context?.row?.obj | nodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
<ng-template let-value="value">
<span title="{{ value }} bytes">{{ value | adfFileSize }}</span>
</ng-template>
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!isEmpty">
<adf-pagination
[pagination]="pagination"
(change)="load(true, $event)">
</adf-pagination>
</ng-container>
</alfresco-upload-drag-area>
</div>
</div>

View File

@@ -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 = <any>{ id: '1' };
nodeActionsService.contentCopied.next(<any>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 = <any>{ id: '3' };
nodeActionsService.contentCopied.next(<any>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 = <any>{ id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
expect(component.load).toHaveBeenCalled();
});
it('does not call refresh on fileUploadComplete event if parent mismatch', () => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = <any>{ id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
expect(component.load).not.toHaveBeenCalled();
});
it('calls refresh on fileUploadDeleted event if parent node match', () => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = <any>{ id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>file);
expect(component.load).toHaveBeenCalled();
});
it('does not call refresh on fileUploadDeleted event if parent mismatch', () => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = <any>{ id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>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(<any> node);
expect(router.navigate).toHaveBeenCalledWith(['/preview', node.id]);
});
it('navigate if node is folder', () => {
spyOn(component, 'navigate').and.stub();
node.isFolder = true;
component.onNodeDoubleClick(<any> 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 = <any>{ 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 = <any>{ id: 'currentNode' };
expect(component.isLoading).toBe(false);
component.load();
expect(component.isLoading).toBe(false);
});
it('sets data on success', () => {
component.node = <any>{ 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 = <any>{ 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', () => {
(<any>component).node = {
path: {
elements: [ {id: 'node-id'} ]
}
};
component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
});
});
});

View File

@@ -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<MinimalNodeEntryEntity> {
return this.nodesApi.getNode(nodeId);
}
fetchNodes(parentNodeId?: string, options: any = {}): Observable<NodePaging> {
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;
}
}

View File

@@ -0,0 +1,15 @@
<adf-toolbar class="app-menu">
<adf-toolbar-title>
<a
class="app-menu__title"
title="{{ appTitle }}"
attr.data-build-number="{{ appBuildNumber }}"
[routerLink]="[ '/' ]">{{ appTitle }}</a>
</adf-toolbar-title>
<app-search></app-search>
<adf-toolbar-divider></adf-toolbar-divider>
<app-current-user></app-current-user>
</adf-toolbar>

View File

@@ -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;
}
}
}

View File

@@ -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);
});
});

View File

@@ -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 <string>this.appConfig.get('application.name');
}
get appBuildNumber(): string {
return <string>this.appConfig.get('application.build');
}
get appTitle(): string {
const { appName, appBuildNumber } = this;
return `${appName} (Build #${appBuildNumber})`;
}
toggleContrast() {
this.enhancedContrast = !this.enhancedContrast;
}
}

View File

@@ -0,0 +1,15 @@
<div class="layout">
<alfresco-upload-drag-area
[rootFolderId]="node?.id"
[disabled]="!canCreateContent(node)"
[showNotificationBar]="false">
<app-header></app-header>
<div class="layout__content">
<app-sidenav class="layout__content-side"></app-sidenav>
<router-outlet></router-outlet>
</div>
<adf-file-uploading-dialog position="left"></adf-file-uploading-dialog>
</alfresco-upload-drag-area>
</div>

View File

@@ -0,0 +1,8 @@
:host {
display: flex;
flex: 1;
router-outlet {
flex: 0 0;
}
}

View File

@@ -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<LayoutComponent>;
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 = <MinimalNodeEntryEntity>{ 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(<any>{});
expect(permission).toBe(true);
});
it('returns false if node does not have permission', () => {
spyOn(contentService, 'hasPermission').and.returnValue(false);
const permission = component.canCreateContent(<any>{});
expect(permission).toBe(false);
});
it('returns false if node is null', () => {
const permission = component.canCreateContent(null);
expect(permission).toBe(false);
});
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,58 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb root="APP.BROWSE.LIBRARIES.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
currentFolderId="-sites-"
selectionMode="none"
[navigate]="false"
[sorting]="[ 'name', 'asc' ]"
[pageSize]="25"
[contextMenuActions]="true"
[contentActions]="false"
(node-dblclick)="onNodeDoubleClick($event)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="title"
title="APP.DOCUMENT_LIST.COLUMNS.TITLE"
class="app-name-column">
<ng-template let-context>
<span title="{{ makeLibraryTooltip(context.row.obj.entry) }}">
{{ makeLibraryTitle(context.row.obj.entry) }}
</span>
</ng-template>
</data-column>
<data-column
key="visibility"
title="APP.DOCUMENT_LIST.COLUMNS.STATUS">
<ng-template let-value="value">
<span *ngIf="(value == 'PUBLIC')" title="{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}">
{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}
</span>
<span *ngIf="(value == 'PRIVATE')" title="{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}">
{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}
</span>
<span *ngIf="(value == 'MODERATED')" title="{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}">
{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}
</span>
</ng-template>
</data-column>
</data-columns>
</adf-document-list>
</div>
</div>

View File

@@ -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 = <any> {
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: [<any>{ 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: [<any>{ 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();
});
});
});

View File

@@ -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
}
}

View File

@@ -25,120 +25,120 @@ import { LoginModule } from 'ng2-alfresco-login';
import { LoginComponent } from './login.component'; import { LoginComponent } from './login.component';
describe('LoginComponent', () => { describe('LoginComponent', () => {
let router: Router; let router: Router;
let route: ActivatedRoute; let route: ActivatedRoute;
let authService: AlfrescoAuthenticationService; let authService: AlfrescoAuthenticationService;
let userPrefService: UserPreferencesService; let userPrefService: UserPreferencesService;
class TestConfig { class TestConfig {
private testBed; private testBed;
private componentInstance; private componentInstance;
private fixture; private fixture;
constructor(config: any = {}) { constructor(config: any = {}) {
const routerProvider = { const routerProvider = {
provide: Router, provide: Router,
useValue: { useValue: {
navigateByUrl: jasmine.createSpy('navigateByUrl'), navigateByUrl: jasmine.createSpy('navigateByUrl'),
navigate: jasmine.createSpy('navigate') 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 = { get userPrefService() {
provide: AlfrescoAuthenticationService, return TestBed.get(UserPreferencesService);
useValue: {
isEcmLoggedIn: jasmine.createSpy('navigateByUrl')
.and.returnValue(config.isEcmLoggedIn || false)
} }
};
const preferencesProvider = { get authService() {
provide: UserPreferencesService, return TestBed.get(AlfrescoAuthenticationService);
useValue: {
setStoragePrefix: jasmine.createSpy('setStoragePrefix')
} }
};
this.testBed = TestBed.configureTestingModule({ get routerService() {
imports: [ return TestBed.get(Router);
RouterTestingModule, }
LoginModule
],
declarations: [
LoginComponent
],
providers: [
routerProvider,
authProvider,
preferencesProvider,
{
provide: ActivatedRoute,
useValue: {
params: Observable.of({ redirect: config.redirect })
}
}
]
});
this.fixture = TestBed.createComponent(LoginComponent); get component() {
this.componentInstance = this.fixture.componentInstance; return this.componentInstance;
this.fixture.detectChanges(); }
} }
get userPrefService() { it('load app when user is already logged in', () => {
return TestBed.get(UserPreferencesService); const testConfig = new TestConfig({
} isEcmLoggedIn: true
});
get authService() { expect(testConfig.routerService.navigateByUrl).toHaveBeenCalled();
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(); it('requires user to be logged in', () => {
}); const testConfig = new TestConfig({
isEcmLoggedIn: false,
redirect: '/personal-files'
});
it('requires user to be logged in', () => { expect(testConfig.routerService.navigate).toHaveBeenCalledWith(['/login', {}]);
const testConfig = new TestConfig({
isEcmLoggedIn: false,
redirect: '/personal-files'
}); });
expect(testConfig.routerService.navigate).toHaveBeenCalledWith(['/login', {}]); describe('onLoginSuccess()', () => {
}); let testConfig;
describe('onLoginSuccess()', () => { beforeEach(() => {
let testConfig; testConfig = new TestConfig({
isEcmLoggedIn: false,
redirect: 'somewhere-over-the-rainbow'
});
});
beforeEach(() => { it('redirects on success', () => {
testConfig = new TestConfig({ testConfig.component.onLoginSuccess();
isEcmLoggedIn: false,
redirect: 'somewhere-over-the-rainbow' 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');
});
});
}); });

View File

@@ -22,48 +22,48 @@ import { Validators } from '@angular/forms';
import { AlfrescoAuthenticationService, UserPreferencesService } from 'ng2-alfresco-core'; import { AlfrescoAuthenticationService, UserPreferencesService } from 'ng2-alfresco-core';
const skipRedirectUrls: string[] = [ const skipRedirectUrls: string[] = [
'/logout', '/logout',
'/personal-files' '/personal-files'
]; ];
@Component({ @Component({
templateUrl: './login.component.html' templateUrl: './login.component.html'
}) })
export class LoginComponent { export class LoginComponent {
private redirectUrl = ''; private redirectUrl = '';
constructor( constructor(
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private auth: AlfrescoAuthenticationService, private auth: AlfrescoAuthenticationService,
private userPreferences: UserPreferencesService private userPreferences: UserPreferencesService
) { ) {
if (auth.isEcmLoggedIn()) { if (auth.isEcmLoggedIn()) {
this.redirect(); 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) => { redirect() {
if (skipRedirectUrls.indexOf(params.redirect) > -1) { this.router.navigateByUrl(this.redirectUrl || '');
const remainingParams = Object.assign({}, params); }
delete remainingParams.redirect; onLoginSuccess(data) {
if (data && data.username) {
router.navigate(['/login', remainingParams]); this.userPreferences.setStoragePrefix(data.username);
} }
this.redirect();
this.redirectUrl = params.redirect;
});
}
redirect() {
this.router.navigateByUrl(this.redirectUrl || '');
}
onLoginSuccess(data) {
if (data && data.username) {
this.userPreferences.setStoragePrefix(data.username);
} }
this.redirect();
}
} }

View File

@@ -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);
});
});
});

View File

@@ -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<MinimalNodeEntity>): boolean {
return selection && selection.length > 0;
}
filesOnlySelected(selection: Array<MinimalNodeEntity>): boolean {
if (this.hasSelection(selection)) {
return selection.every(entity => entity.entry && entity.entry.isFile);
}
return false;
}
foldersOnlySelected(selection: Array<MinimalNodeEntity>): boolean {
if (this.hasSelection(selection)) {
return selection.every(entity => entity.entry && entity.entry.isFolder);
}
return false;
}
isFileSelected(selection: Array<MinimalNodeEntity>): boolean {
if (selection && selection.length === 1) {
let entry = selection[0].entry;
if (entry && entry.isFile) {
return true;
}
}
return false;
}
canEditFolder(selection: Array<MinimalNodeEntity>): 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<MinimalNodeEntity> = []): boolean {
return selection.every(node => node.entry && this.nodeHasPermission(node.entry, 'delete'));
}
canMove(selection: Array<MinimalNodeEntity>): boolean {
return this.canDelete(selection);
}
canPreviewFile(selection: Array<MinimalNodeEntity>): boolean {
return this.isFileSelected(selection);
}
canShareFile(selection: Array<MinimalNodeEntity>): boolean {
return this.isFileSelected(selection);
}
canDownloadFile(selection: Array<MinimalNodeEntity>): boolean {
return this.isFileSelected(selection);
}
nodeHasPermission(node: MinimalNodeEntryEntity, permission: string) {
if (node && permission) {
const { allowableOperations = [] } = <any>(node || {});
if (allowableOperations.indexOf(permission) > -1) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,3 @@
<ng-container *ngIf="nodeId">
<adf-viewer [fileNodeId]="nodeId"></adf-viewer>
</ng-container>

View File

@@ -0,0 +1,4 @@
.app-preview {
width: 100%;
height: 100%;
}

View File

@@ -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])
);
}
});
}
}

View File

@@ -0,0 +1,122 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb root="APP.BROWSE.RECENT.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
md-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(documentList.selection[0]?.entry?.id)">
<md-icon>open_in_browser</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[app-download-node]="documentList.selection">
<md-icon>get_app</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[mdMenuTriggerFor]="actionsMenu">
<md-icon>more_vert</md-icon>
</button>
<md-menu #actionsMenu="mdMenu"
[overlapTrigger]="false"
class="secondary-options">
<button
md-menu-item
#favorite="favorite"
[app-favorite-node]="documentList.selection">
<md-icon [ngClass]="{ 'icon-highlight': favorite.hasFavorites() }">
{{ favorite.hasFavorites() ? 'star' :'star_border' }}
</md-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
md-menu-item
[app-copy-node]="documentList.selection">
<md-icon>content_copy</md-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<md-icon>library_books</md-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<md-icon>delete</md-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
</md-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
currentFolderId="-recent-"
selectionMode="multiple"
[navigate]="false"
[sorting]="[ 'modifiedAt', 'desc' ]"
[pageSize]="25"
[contextMenuActions]="true"
[contentActions]="false"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="name"
class="app-name-column"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span title="{{ context?.row?.obj | nodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION"
type="location"
format="/personal-files">
</data-column>
<data-column
key="content.sizeInBytes"
type="fileSize"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
</data-column>
<data-column
key="modifiedAt"
type="date"
format="timeAgo"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
</data-column>
</data-columns>
</adf-document-list>
</div>
</div>

View File

@@ -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();
});
});
});

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,6 @@
<adf-search-control
[searchTerm]="searchTerm"
[autocomplete]="false"
[highlight]="true"
(fileSelect)="onNodeClicked($event)">
</adf-search-control>

View File

@@ -0,0 +1,9 @@
@import './../../ui/variables';
adf-search-control {
color: $alfresco-white;
}
:host {
height: $app-menu-height;
}

View File

@@ -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();
});
});
});

View File

@@ -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 ]);
}
}
}
}

View File

@@ -0,0 +1,130 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb root="APP.BROWSE.SHARED.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
md-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(documentList.selection[0]?.entry?.nodeId)">
<md-icon>open_in_browser</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[app-download-node]="documentList.selection">
<md-icon>get_app</md-icon>
</button>
<button
md-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[mdMenuTriggerFor]="actionsMenu">
<md-icon>more_vert</md-icon>
</button>
<md-menu #actionsMenu="mdMenu"
[overlapTrigger]="false"
class="secondary-options">
<button
md-menu-item
#favorite="favorite"
[app-favorite-node]="documentList.selection">
<md-icon [ngClass]="{ 'icon-highlight': favorite.hasFavorites() }">
{{ favorite.hasFavorites() ? 'star' :'star_border' }}
</md-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
md-menu-item
[app-copy-node]="documentList.selection">
<md-icon>content_copy</md-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<md-icon>library_books</md-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
md-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<md-icon>delete</md-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
</md-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
currentFolderId="-sharedlinks-"
selectionMode="multiple"
[sorting]="[ 'modifiedAt', 'desc' ]"
[pageSize]="25"
[contextMenuActions]="true"
[contentActions]="false"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="name"
class="app-name-column"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span title="{{ context?.row?.obj | nodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION"
type="location"
format="/personal-files">
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
type="date"
format="timeAgo"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
</data-column>
<data-column
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
<data-column
key="sharedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.SHARED_BY">
</data-column>
</data-columns>
</adf-document-list>
</div>
</div>

View File

@@ -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();
});
});
});

View File

@@ -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<MinimalNodeEntity>): boolean {
return selection && selection.length === 1;
}
refresh(): void {
if (this.documentList) {
this.documentList.reload();
}
}
}

View File

@@ -0,0 +1,68 @@
<div class="sidenav">
<div class="sidenav__section sidenav__section--new">
<button class="sidenav__section--new__button" md-button [mdMenuTriggerFor]="menu">
<span>{{ 'APP.NEW_MENU.LABEL' | translate }}</span>
<md-icon>arrow_drop_down</md-icon>
</button>
<md-menu #menu="mdMenu" [overlapTrigger]="false">
<button
md-menu-item
[app-create-folder]="node?.id"
[disabled]="!canCreateContent(node)"
title="{{
( canCreateContent(node)
? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER'
: 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED'
) | translate
}}">
<md-icon>create_new_folder</md-icon>
<span>{{ 'APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER' | translate }}</span>
</button>
<alfresco-upload-button
[showNotificationBar]="false"
tooltip="{{
(canCreateContent(node)
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED'
) | translate }}"
[disabled]="!canCreateContent(node)"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="false"
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE' | translate">
</alfresco-upload-button>
<alfresco-upload-button
[showNotificationBar]="false"
tooltip="{{
(canCreateContent(node)
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED'
) | translate }}"
[disabled]="!canCreateContent(node)"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="true"
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER' | translate">
</alfresco-upload-button>
</md-menu>
</div>
<div class="sidenav__section" *ngFor="let list of menus;">
<ul class="sidenav-menu">
<li class="sidenav-menu__item" *ngFor="let item of list">
<!-- [routerLinkActive]="'sidenav-menu__item-link--active'" -->
<a
class="sidenav-menu__item-link"
[routerLink]="item.route.url"
[ngClass]="{ 'disabled': item.disabled }"
title="{{ item.title || '' | translate }}">
<md-icon>{{ item.icon }}</md-icon>
{{ item.label | translate }}
</a>
</li>
</ul>
</div>
</div>

View File

@@ -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;
}
}
}
}
}
}

View File

@@ -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(<any>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');
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,89 @@
<div class="inner-layout">
<div class="inner-layout__header">
<adf-breadcrumb [root]="'APP.BROWSE.TRASHCAN.TITLE' | translate">
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
md-icon-button
[app-permanent-delete-node]="documentList.selection"
(selection-node-deleted)="refresh()"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.DELETE' | translate }}">
<md-icon>delete_forever</md-icon>
</button>
<button
md-icon-button
(selection-node-restored)="refresh()"
[app-restore-node]="documentList.selection"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.RESTORE' | translate }}">
<md-icon>restore</md-icon>
</button>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
currentFolderId="-trashcan-"
selectionMode="multiple"
[navigate]="false"
[sorting]="[ 'archivedAt', 'desc' ]"
[pageSize]="25"
[contextMenuActions]="true"
[contentActions]="false">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="name"
class="app-name-column"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span title="{{ context?.row?.obj | nodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path.name"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-value="value">
<span title="{{ value }}">{{ (value || '').split('/').pop() }}</span>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
<ng-template let-value="value">
<span title="{{ value }} bytes">{{ value | adfFileSize }}</span>
</ng-template>
</data-column>
<data-column
key="archivedAt"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
key="archivedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_BY">
</data-column>
</data-columns>
</adf-document-list>
</div>
</div>

View File

@@ -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();
});
});
});

View File

@@ -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();
}
}

68
src/app/ui/_layout.scss Normal file
View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
@import './_variables-color.scss';
$app-menu-height: 64px;

View File

@@ -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';

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,5 @@
@import '../variables';
.adf-breadcrumb {
width: 0;
}

View File

@@ -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;
}
}

25
src/app/ui/theme.scss Normal file
View File

@@ -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);

201
src/assets/i18n/en.json Normal file
View File

@@ -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."
}
}
}
}
}
}

125
src/assets/i18n/ru.json Normal file
View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 636.47998 172.92"
height="172.92"
width="636.47998"
xml:space="preserve"
id="svg2"
version="1.1"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><g
transform="matrix(1.3333333,0,0,-1.3333333,0,172.92)"
id="g10"><g
transform="scale(0.1)"
id="g12"><path
id="path14"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 1722.35,939.109 146.43,-391.98 H 1570.67 Z M 1379.69,247.27 h 77.46 l 91.24,237.726 h 344.82 l 92.3,-237.726 h 77.43 l -297.06,764.71 h -80.66 L 1379.69,247.27" /><path
id="path16"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 2088.94,247.262 h 66.8086 v 764.715 H 2088.94 Z" /><path
id="path18"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 2441.98,799.898 H 2333.8 v 48.188 c 0,15.016 0.33,28.937 1.05,41.762 0.7,12.898 3.35,24.113 7.95,33.777 4.58,9.617 11.84,17.285 21.73,23.016 9.9,5.703 24.05,8.57 42.46,8.57 7.75,0 15.57,-0.535 23.36,-1.598 7.71,-1.074 16.22,-2.328 25.4,-3.754 v 57.801 c -10.59,1.44 -20.11,2.55 -28.6,3.2 -8.51,0.71 -17.7,1.12 -27.61,1.12 -26.88,0 -48.98,-3.95 -66.31,-11.79 -17.33,-7.838 -30.96,-18.917 -40.85,-33.178 -9.91,-14.324 -16.58,-31.453 -20.16,-51.407 -3.52,-20.015 -5.32,-41.8 -5.32,-65.351 v -50.356 h -93.33 v -56.73 h 93.33 V 247.27 h 66.9 v 495.898 h 108.18 v 56.73" /><path
id="path20"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 2462.85,247.27 h 66.85 v 290.023 c 0,28.086 4.92,54.504 14.87,79.113 9.87,24.614 23.84,45.676 41.87,63.289 18.09,17.567 39.47,31.133 64.21,40.629 24.74,9.524 52.34,13.508 82.73,12.145 v 67.433 c -49.49,2.145 -92.11,-7.644 -127.84,-29.437 -35.7,-21.82 -62.02,-55.149 -79.04,-100.156 h -2.09 v 129.593 h -61.56 V 247.27" /><path
id="path22"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 2765.34,562.16 c 2.82,22.965 8.63,45.395 17.51,67.313 8.85,22.039 20.64,41.398 35.51,58.086 14.86,16.699 32.89,30.168 54.17,40.289 21.18,10.175 45.56,15.32 73.17,15.32 26.88,0 50.9,-5.145 72.15,-15.32 21.24,-10.121 39.26,-23.59 54.11,-40.289 14.81,-16.688 26.32,-35.95 34.45,-57.5 8.14,-21.711 12.57,-44.325 13.3,-67.899 z m 421.18,-56.805 c 1.43,36.543 -2.12,72.614 -10.63,108.149 -8.43,35.582 -22.4,67.059 -41.88,94.508 -19.46,27.39 -44.7,49.578 -75.87,66.519 -31.1,16.922 -68.59,25.367 -112.44,25.367 -43.85,0 -81.34,-7.933 -112.48,-23.781 -31.11,-15.847 -56.58,-36.754 -76.39,-62.777 -19.75,-25.942 -34.46,-55.465 -44.04,-88.535 -9.5,-33.121 -14.3,-66.825 -14.3,-101.227 0,-37.105 4.8,-72.359 14.3,-105.758 9.58,-33.402 24.29,-62.754 44.04,-88.097 19.81,-25.293 45.28,-45.332 76.39,-60.192 31.14,-14.89 68.63,-22.261 112.48,-22.261 69.3,0 122.86,17.3 160.71,51.929 37.88,34.617 63.86,83.379 77.99,146.219 h -66.82 c -10.62,-42.164 -29.7,-76.25 -57.28,-102.313 -27.6,-26.035 -65.79,-39.082 -114.6,-39.082 -31.82,0 -59.22,6.254 -82.23,18.797 -22.98,12.571 -41.57,28.489 -55.71,47.641 -14.14,19.164 -24.76,40.703 -31.78,64.496 -7.13,23.82 -10.64,47.273 -10.64,70.398 h 421.18" /><path
id="path24"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 3629.63,638.176 c -1.41,29.961 -7.43,56.047 -18.02,78.183 -10.62,22.114 -24.93,40.692 -42.93,55.684 -18.06,15.008 -38.95,26.086 -62.64,33.195 -23.71,7.168 -49.36,10.739 -76.92,10.739 -24.04,0 -48.26,-2.852 -72.67,-8.559 -24.41,-5.727 -46.5,-14.676 -66.26,-26.816 -19.89,-12.106 -35.97,-28.207 -48.32,-48.125 -12.38,-20.032 -18.6,-43.997 -18.6,-71.825 0,-23.574 3.88,-43.371 11.67,-59.461 7.79,-16.031 18.61,-29.609 32.35,-40.668 13.8,-11.093 29.9,-20.355 48.32,-27.867 18.38,-7.484 38.52,-14.078 60.46,-19.797 l 85.95,-19.269 c 14.83,-3.621 29.54,-7.828 44.02,-12.863 14.52,-4.985 27.42,-11.227 38.76,-18.747 11.3,-7.484 20.29,-16.945 27.03,-28.367 6.72,-11.406 10.1,-24.976 10.1,-40.738 0,-19.262 -4.82,-35.508 -14.31,-48.699 -9.59,-13.25 -21.82,-24.098 -36.61,-32.692 -14.88,-8.523 -31.17,-14.605 -48.83,-18.144 -17.66,-3.629 -34.32,-5.41 -49.89,-5.41 -44.57,0 -81.84,11.609 -111.93,34.828 -30.06,23.211 -46.51,57.672 -49.37,103.347 h -66.85 c 5.68,-67.875 28.52,-117.304 68.47,-148.324 40,-31.051 92.15,-46.582 156.51,-46.582 25.48,0 51.28,3 77.44,9.063 26.18,6.09 49.7,15.699 70.55,28.949 20.87,13.199 37.99,30.328 51.49,51.437 13.39,21.055 20.14,46.543 20.14,76.532 0,24.316 -4.59,45.371 -13.79,63.254 -9.18,17.796 -21.04,32.777 -35.53,44.929 -14.51,12.117 -31.13,21.969 -49.89,29.477 -18.7,7.461 -37.7,12.687 -56.74,15.515 l -89.15,20.372 c -11.31,2.828 -23.66,6.558 -37.11,11.226 -13.44,4.598 -25.85,10.531 -37.12,17.699 -11.34,7.071 -20.73,15.836 -28.17,26.211 -7.41,10.336 -11.05,23.028 -11.05,38.02 0,17.785 3.81,32.863 11.6,44.98 7.72,12.164 18.05,21.965 30.82,29.477 12.67,7.473 26.65,12.785 41.85,16.055 15.23,3.242 30.27,4.789 45.12,4.789 19.09,0 37.1,-2.254 54.12,-6.961 16.91,-4.61 32.01,-11.91 45.07,-21.953 13.06,-9.95 23.52,-22.637 31.3,-38.02 7.79,-15.324 11.98,-33.352 12.71,-54.074 h 66.85" /><path
id="path26"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 4134.03,610.336 c -7.77,63.559 -32.02,111.051 -72.7,142.426 -40.72,31.426 -90.39,47.14 -149.06,47.14 -41.77,0 -78.71,-7.386 -110.9,-22.269 -32.18,-14.836 -59.27,-34.754 -81.17,-59.695 -21.91,-25.004 -38.5,-54.356 -49.84,-88.086 -11.32,-33.731 -16.99,-69.164 -16.99,-106.27 0,-37.789 5.67,-73.348 16.99,-106.785 11.34,-33.402 27.93,-62.57 49.84,-87.563 21.9,-24.988 48.99,-44.843 81.17,-59.703 32.19,-14.883 69.13,-22.261 110.9,-22.261 61.53,0 112.24,18.929 152.21,56.757 39.99,37.836 64.57,90.301 73.75,157.422 h -66.83 c -2.1,-22.859 -7.8,-43.91 -16.99,-63.156 -9.22,-19.297 -20.88,-35.875 -34.95,-49.832 -14.18,-13.922 -30.49,-24.832 -48.84,-32.66 -18.35,-7.875 -37.88,-11.774 -58.35,-11.774 -31.86,0 -59.81,6.325 -83.86,18.907 -24.07,12.64 -44,29.293 -59.92,49.941 -15.9,20.613 -27.92,44.105 -36.09,70.324 -8.12,26.317 -12.2,53.114 -12.2,80.383 0,27.305 4.08,54.035 12.2,80.356 8.17,26.218 20.19,49.71 36.09,70.324 15.92,20.648 35.85,37.273 59.92,49.918 24.05,12.675 52,18.992 83.86,18.992 43.81,0 78.32,-11.801 103.41,-35.34 25.17,-23.602 42.26,-56.086 51.48,-97.496 h 66.87" /><path
id="path28"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 4421.11,743.168 c 31.84,0 59.81,-6.316 83.82,-18.98 24.06,-12.657 44.02,-29.282 59.97,-49.918 15.9,-20.625 27.94,-44.118 36.07,-70.336 8.11,-26.321 12.23,-53.039 12.23,-80.36 0,-27.254 -4.12,-54.058 -12.23,-80.367 -8.13,-26.219 -20.17,-49.723 -36.07,-70.336 -15.95,-20.637 -35.91,-37.289 -59.97,-49.93 -24.01,-12.589 -51.98,-18.918 -83.82,-18.918 -31.83,0 -59.78,6.329 -83.78,18.918 -24.06,12.641 -44.03,29.293 -60.02,49.93 -15.83,20.613 -27.89,44.117 -36.01,70.336 -8.13,26.309 -12.18,53.113 -12.18,80.367 0,27.321 4.05,54.039 12.18,80.36 8.12,26.218 20.18,49.711 36.01,70.336 15.99,20.636 35.96,37.261 60.02,49.918 24,12.664 51.95,18.98 83.78,18.98 m 0,56.73 c -41.71,0 -78.66,-7.386 -110.87,-22.257 -32.15,-14.836 -59.25,-34.766 -81.12,-59.707 -21.98,-25.004 -38.57,-54.344 -49.89,-88.086 -11.3,-33.731 -17,-69.153 -17,-106.274 0,-37.773 5.7,-73.344 17,-106.781 11.32,-33.402 27.91,-62.57 49.89,-87.563 21.87,-24.988 48.97,-44.851 81.12,-59.699 32.21,-14.89 69.16,-22.261 110.87,-22.261 41.76,0 78.72,7.371 110.89,22.261 32.2,14.848 59.25,34.711 81.2,59.699 21.91,24.993 38.52,54.161 49.83,87.563 11.3,33.437 16.98,69.008 16.98,106.781 0,37.121 -5.68,72.543 -16.98,106.274 -11.31,33.742 -27.92,63.082 -49.83,88.086 -21.95,24.941 -49,44.871 -81.2,59.707 -32.17,14.871 -69.13,22.257 -110.89,22.257" /><path
id="path30"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 4719.6,765.938 c 9.17,0 18.16,0.289 18.16,10.785 0,8.593 -7.06,10.605 -14.34,10.605 h -13.6 v -21.39 z m -9.78,-33.805 h -7.5 v 61.644 h 23.16 c 13.65,0 19.71,-6.007 19.71,-17.054 0,-10.934 -7.03,-15.797 -15.16,-17.2 l 17.95,-27.39 h -8.65 l -17.06,27.39 h -12.45 z m -32.64,30.84 c 0,-25.524 18.78,-45.75 44.13,-45.75 25.31,0 44.17,20.226 44.17,45.75 0,25.465 -18.86,45.675 -44.17,45.675 -25.35,0 -44.13,-20.21 -44.13,-45.675 m 96.4,0 c 0,-30 -22.73,-53.332 -52.27,-53.332 -29.55,0 -52.29,23.332 -52.29,53.332 0,29.902 22.74,53.308 52.29,53.308 29.54,0 52.27,-23.406 52.27,-53.308" /><path
id="path32"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.125 -179.363,183.094 -6.508,6.668 c -102.801,105.187 -270.328,105.187 -372.6367,0.414 -103.1407,-104.699 -103.1407,-274.969 0,-379.778 102.3087,-105.089 269.3867,-105.089 372.1487,0 l 186.359,189.602" /><path
id="path34"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 635.914,648.371 V 389.75 l 0.379,-9.508 c 0,-148.262 -118.109,-269.191 -263.695,-269.191 -145.598,0 -263.68,120.16 -263.68,268.433 0,148.461 118.523,268.887 263.68,268.887 h 263.316" /><path
id="path36"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 635.914,648.371 V 389.75 l 0.379,-9.508 c 0,-148.262 -118.109,-269.191 -263.695,-269.191 -145.598,0 -263.68,120.16 -263.68,268.433 0,148.461 118.523,268.887 263.68,268.887 h 263.316" /><path
id="path38"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 635.914,648.371 V 389.75 l 0.379,-9.508 c 0,-148.262 -118.109,-269.191 -263.695,-269.191 -145.598,0 -263.68,120.16 -263.68,268.433 0,16.532 1.73,32.547 4.437,47.957 102.735,-72.933 244.848,-62.582 336.2,31 l 186.359,189.688 v 0.242" /><path
id="path40"
style="fill:#7dc629;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 372.598,614.484 c -126.864,0 -230,-105.203 -230,-234.988 0,-129.305 103.136,-234.266 230,-234.266 127.285,0 230.035,104.961 230.035,234.266 V 614.484 H 372.598" /><path
id="path42"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.371 179.215,-182.773 7.375,-6.668 c 102.383,-105.043 102.859,-275.571 0,-380.3206 -103.141,-104.8203 -269.762,-104.8203 -372.949,0 -102.762,104.7496 -102.762,275.0816 0,380.0856 l 186.359,189.676" /><path
id="path44"
style="fill:#7dc629;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 473.602,434.59 c -90.024,-91.598 -90.024,-240.41 0,-331.738 89.609,-91.6332 235.328,-91.6332 325.351,0 89.633,91.328 89.633,240.14 0,331.738 l -7.496,6.859 L 635.91,600.273 473.602,434.895 v -0.305" /><path
id="path46"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.371 h 253.953 l 9.238,0.453 c 145.645,0 263.715,-120.379 263.715,-268.824 0,-148.551 -117.68,-268.441 -263.273,-268.441 -145.598,0 -263.633,120.101 -263.633,268.441 v 268.371" /><path
id="path48"
style="fill:#7dc629;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 669.523,379.996 c 0,-129.617 102.715,-234.586 230.028,-234.586 126.729,0 229.469,104.969 229.469,234.586 0,129.785 -102.74,234.484 -229.469,234.484 H 669.523 V 379.996" /><path
id="path50"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.371 179.215,183.031 6.949,6.891 c 102.809,104.781 269.802,105.199 372.582,0.473 102.81,-105.051 102.81,-275.051 0,-380.071 -102.78,-105.004 -269.773,-105.004 -372.582,0 L 635.914,648.371" /><path
id="path52"
style="fill:#7dc629;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 846.113,482.984 c 89.246,-91.863 234.867,-91.863 324.917,0 90.09,91.438 90.09,239.836 0,331.52 -90.05,91.582 -235.671,91.582 -324.917,0 l -7.375,-7.156 -155.273,-158.969 161.836,-165.199 0.812,-0.196" /><path
id="path54"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.371 v 268.461 c 0,148.158 117.559,268.798 263.633,268.798 145.203,0 262.813,-119.86 262.813,-268.36 0,-148.485 -117.61,-268.899 -262.813,-268.899 H 635.914" /><path
id="path56"
style="fill:#f79427;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 899.543,682.496 c 126.727,0 229.487,105.016 229.487,234.731 0,129.233 -102.76,234.483 -229.487,234.483 -127.301,0 -230.488,-105.25 -230.488,-234.483 l 0.465,-10.043 V 682.496 h 230.023" /><path
id="path58"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 635.914,648.371 -179.363,183.031 -6.996,6.489 c -102.762,105.183 -102.762,275.549 0,380.309 102.332,104.96 269.41,104.96 372.523,0 102.809,-104.76 102.809,-275.126 0,-379.907 L 635.914,648.371" /><path
id="path60"
style="fill:#3a7fca;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 798.48,860.152 c 89.594,91.621 89.594,240.308 0,331.798 -89.988,91.55 -235.281,91.55 -325.289,0 -90.086,-91.49 -90.086,-240.177 0,-331.798 l 6.559,-7.019 156.164,-158.555 162.078,165.172 0.488,0.402" /><path
id="path62"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 635.914,646.18 H 382.18 l -9.582,-0.254 c -145.157,0 -263.68,120.426 -263.68,268.812 0,148.282 118.082,268.672 263.23,268.672 145.622,0 263.766,-120.39 263.766,-268.769 V 646.18" /><path
id="path64"
style="fill:#3a7fca;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 602.598,914.641 c 0,129.599 -102.75,234.489 -230.442,234.489 -126.789,0 -229.937,-104.89 -229.937,-234.317 0,-129.52 103.148,-234.766 229.937,-234.766 l 10.02,0.266 h 220.422 v 234.328" /><path
id="path66"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 635.914,646.18 0,645.926 c 0,68.691 25.3438,137.515 77.4063,190.176 102.3087,104.773 269.3867,104.773 372.1487,0 L 635.914,646.18" /><path
id="path68"
style="fill:#3a7fca;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 425.945,811.992 588.633,646.184 H 372.59 c -116.742,0 -216.031,-77.598 -250.504,-185.43 -7.047,6.215 -14.492,12.527 -20.988,19.598 -90.098,91.632 -90.098,239.8 0,331.64 89.156,91.485 235.242,91.485 324.847,0" /><path
id="path70"
style="fill:#2a59ab;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 426.355,811.793 555.094,680.445 371.988,679.887 c -105.394,0 -194.261,72.203 -221.222,170.781 87.414,49.953 201.902,37.945 275.175,-38.68 l 0.414,-0.195" /><path
id="path72"
style="fill:#2a59ab;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 602.676,914.176 -0.184,-185.778 -129.25,131.786 c -74.422,76.054 -87.523,191.106 -38.535,280.266 96.617,-27.6 167.969,-118.16 167.969,-225.79 v -0.484" /><path
id="path74"
style="fill:#2a59ab;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 668.531,727.984 v 186.36 c 0,107.386 71.227,198.086 167.969,225.916 48.93,-89.41 39.727,-201.475 -37.07,-278.979 L 668.531,727.984" /><path
id="path76"
style="fill:#fae033;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 716.563,682.402 845.68,814.406 c 74.383,75.992 187.33,88.844 274.83,38.949 -27.62,-98.519 -111.89,-169.296 -215.612,-171.023 l -188.335,0.07" /><path
id="path78"
style="fill:#45ab3d;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 716.543,614.613 h 182.75 c 105.397,0 194.187,-71.761 221.317,-170.523 -87.95,-50.141 -200.837,-37 -275.551,38.957 L 716.543,614.613" /><path
id="path80"
style="fill:#45ab3d;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 669.504,381.234 V 566.691 L 798.949,435.113 c 74.43,-76.187 87.035,-191.781 38.094,-280.941 -96.656,27.719 -167.539,118.66 -167.539,226.172 v 0.89" /><path
id="path82"
style="fill:#45ab3d;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 473.906,435.039 128.653,131.215 0.293,-186.434 c 0,-107.461 -70.786,-197.769 -167.442,-225.621 -48.953,89.34 -35.851,204.653 38.496,280.461 v 0.379" /><path
id="path84"
style="fill:#45ab3d;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 373.902,614.555 H 555.824 L 426.816,482.797 c -74.273,-75.899 -188.101,-88.988 -275.457,-39.082 27.465,98.808 116.356,170.84 221.739,170.84 h 0.804" /></g></g></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,6 +1,7 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
@import '~ng2-alfresco-core/prebuilt-themes/adf-blue-orange.css'; @import '~ng2-alfresco-core/prebuilt-themes/adf-blue-orange.css';
@import 'app/ui/application';
body, html { body, html {
height: 100%; height: 100%;