mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-07-24 17:31:52 +00:00
main application
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.css"
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/pdfjs-dist/build/pdf.js",
|
||||
@@ -41,6 +41,11 @@
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
},
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"app/ui"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@@ -4,7 +4,7 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
@@ -11,7 +11,6 @@ import { SearchModule } from 'ng2-alfresco-search';
|
||||
|
||||
export function modules() {
|
||||
return [
|
||||
// ADF modules
|
||||
CoreModule,
|
||||
DataTableModule,
|
||||
DocumentListModule,
|
||||
|
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
@@ -1,12 +1,28 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
|
||||
|
||||
import { AdfModule } from './adf.module';
|
||||
import { CommonModule } from './common/common.module';
|
||||
import { MaterialModule } from './common/material.module';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { APP_ROUTES } from './app.routes';
|
||||
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { PreviewComponent } from './components/preview/preview.component';
|
||||
import { FilesComponent } from './components/files/files.component';
|
||||
import { FavoritesComponent } from './components/favorites/favorites.component';
|
||||
import { LibrariesComponent } from './components/libraries/libraries.component';
|
||||
import { RecentFilesComponent } from './components/recent-files/recent-files.component';
|
||||
import { SharedFilesComponent } from './components/shared-files/shared-files.component';
|
||||
import { TrashcanComponent } from './components/trashcan/trashcan.component';
|
||||
import { LayoutComponent } from './components/layout/layout.component';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { CurrentUserComponent } from './components/current-user/current-user.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import { SidenavComponent } from './components/sidenav/sidenav.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -14,13 +30,36 @@ import { LoginComponent } from './components/login/login.component';
|
||||
RouterModule.forRoot(APP_ROUTES, {
|
||||
enableTracing: true
|
||||
}),
|
||||
AdfModule
|
||||
AdfModule,
|
||||
CommonModule,
|
||||
MaterialModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LoginComponent
|
||||
LoginComponent,
|
||||
LayoutComponent,
|
||||
HeaderComponent,
|
||||
CurrentUserComponent,
|
||||
SearchComponent,
|
||||
SidenavComponent,
|
||||
FilesComponent,
|
||||
FavoritesComponent,
|
||||
LibrariesComponent,
|
||||
RecentFilesComponent,
|
||||
SharedFilesComponent,
|
||||
TrashcanComponent,
|
||||
PreviewComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: TRANSLATION_PROVIDER,
|
||||
multi: true,
|
||||
useValue: {
|
||||
name: 'app',
|
||||
source: 'assets'
|
||||
}
|
||||
}
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
@@ -16,10 +16,100 @@
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { AuthGuardEcm } from 'ng2-alfresco-core';
|
||||
|
||||
import { LayoutComponent } from './components/layout/layout.component';
|
||||
|
||||
import { FilesComponent } from './components/files/files.component';
|
||||
import { FavoritesComponent } from './components/favorites/favorites.component';
|
||||
import { LibrariesComponent } from './components/libraries/libraries.component';
|
||||
import { RecentFilesComponent } from './components/recent-files/recent-files.component';
|
||||
import { SharedFilesComponent } from './components/shared-files/shared-files.component';
|
||||
import { TrashcanComponent } from './components/trashcan/trashcan.component';
|
||||
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { PreviewComponent } from './components/preview/preview.component';
|
||||
|
||||
export const APP_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: `/personal-files`,
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
component: FavoritesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.FAVORITES.TITLE'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'libraries',
|
||||
children: [{
|
||||
path: '',
|
||||
component: LibrariesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.LIBRARIES.TITLE'
|
||||
}
|
||||
}, {
|
||||
path: ':id',
|
||||
component: FilesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.LIBRARIES.TITLE'
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
path: 'personal-files',
|
||||
children: [{
|
||||
path: '',
|
||||
component: FilesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.PERSONAL.TITLE',
|
||||
defaultNodeId: '-my-'
|
||||
}
|
||||
}, {
|
||||
path: ':id',
|
||||
component: FilesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.PERSONAL.TITLE'
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
path: 'recent-files',
|
||||
component: RecentFilesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.RECENT.TITLE'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'shared',
|
||||
component: SharedFilesComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.SHARED.TITLE'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'trashcan',
|
||||
component: TrashcanComponent,
|
||||
data: {
|
||||
i18nTitle: 'APP.BROWSE.TRASHCAN.TITLE'
|
||||
}
|
||||
}
|
||||
],
|
||||
canActivate: [
|
||||
AuthGuardEcm
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'preview/:nodeId',
|
||||
component: PreviewComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/login',
|
||||
@@ -33,3 +123,4 @@ export const APP_ROUTES: Routes = [
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
44
src/app/common/adf.module.ts
Normal file
44
src/app/common/adf.module.ts
Normal 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 {}
|
93
src/app/common/common.module.ts
Normal file
93
src/app/common/common.module.ts
Normal 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 {}
|
62
src/app/common/dialogs/folder-dialog.component.html
Normal file
62
src/app/common/dialogs/folder-dialog.component.html
Normal 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>
|
261
src/app/common/dialogs/folder-dialog.component.spec.ts
Normal file
261
src/app/common/dialogs/folder-dialog.component.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
138
src/app/common/dialogs/folder-dialog.component.ts
Normal file
138
src/app/common/dialogs/folder-dialog.component.ts
Normal 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;
|
||||
}
|
||||
}
|
45
src/app/common/dialogs/folder-name.validators.ts
Normal file
45
src/app/common/dialogs/folder-name.validators.ts
Normal 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`
|
||||
};
|
||||
}
|
96
src/app/common/directives/folder-create.directive.spec.ts
Normal file
96
src/app/common/directives/folder-create.directive.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
66
src/app/common/directives/folder-create.directive.ts
Normal file
66
src/app/common/directives/folder-create.directive.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
96
src/app/common/directives/folder-edit.directive.spec.ts
Normal file
96
src/app/common/directives/folder-edit.directive.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
67
src/app/common/directives/folder-edit.directive.ts
Normal file
67
src/app/common/directives/folder-edit.directive.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
249
src/app/common/directives/node-copy.directive.spec.ts
Normal file
249
src/app/common/directives/node-copy.directive.spec.ts
Normal 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]]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
125
src/app/common/directives/node-copy.directive.ts
Normal file
125
src/app/common/directives/node-copy.directive.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
246
src/app/common/directives/node-delete.directive.spec.ts
Normal file
246
src/app/common/directives/node-delete.directive.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
237
src/app/common/directives/node-delete.directive.ts
Normal file
237
src/app/common/directives/node-delete.directive.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
142
src/app/common/directives/node-download.directive.spec.ts
Normal file
142
src/app/common/directives/node-download.directive.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
110
src/app/common/directives/node-download.directive.ts
Normal file
110
src/app/common/directives/node-download.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
332
src/app/common/directives/node-favorite.directive.spec.ts
Normal file
332
src/app/common/directives/node-favorite.directive.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
181
src/app/common/directives/node-favorite.directive.ts
Normal file
181
src/app/common/directives/node-favorite.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
299
src/app/common/directives/node-move.directive.spec.ts
Normal file
299
src/app/common/directives/node-move.directive.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
210
src/app/common/directives/node-move.directive.ts
Normal file
210
src/app/common/directives/node-move.directive.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
194
src/app/common/directives/node-permanent-delete.directive.ts
Normal file
194
src/app/common/directives/node-permanent-delete.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
334
src/app/common/directives/node-restore.directive.spec.ts
Normal file
334
src/app/common/directives/node-restore.directive.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
||||
});
|
259
src/app/common/directives/node-restore.directive.ts
Normal file
259
src/app/common/directives/node-restore.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
41
src/app/common/material.module.ts
Normal file
41
src/app/common/material.module.ts
Normal 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 {}
|
145
src/app/common/pipes/node-name-tooltip.pipe.spec.ts
Normal file
145
src/app/common/pipes/node-name-tooltip.pipe.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
79
src/app/common/pipes/node-name-tooltip.pipe.ts
Normal file
79
src/app/common/pipes/node-name-tooltip.pipe.ts
Normal 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`);
|
||||
}
|
||||
}
|
36
src/app/common/services/browsing-files.service.spec.ts
Normal file
36
src/app/common/services/browsing-files.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
26
src/app/common/services/browsing-files.service.ts
Normal file
26
src/app/common/services/browsing-files.service.ts
Normal 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>();
|
||||
}
|
31
src/app/common/services/content-management.service.ts
Normal file
31
src/app/common/services/content-management.service.ts
Normal 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>();
|
||||
}
|
1156
src/app/common/services/node-actions.service.spec.ts
Normal file
1156
src/app/common/services/node-actions.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
580
src/app/common/services/node-actions.service.ts
Normal file
580
src/app/common/services/node-actions.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
src/app/components/current-user/current-user.component.html
Normal file
18
src/app/components/current-user/current-user.component.html
Normal 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>
|
36
src/app/components/current-user/current-user.component.scss
Normal file
36
src/app/components/current-user/current-user.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
63
src/app/components/current-user/current-user.component.ts
Normal file
63
src/app/components/current-user/current-user.component.ts
Normal 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('');
|
||||
}
|
||||
}
|
135
src/app/components/favorites/favorites.component.html
Normal file
135
src/app/components/favorites/favorites.component.html
Normal 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>
|
189
src/app/components/favorites/favorites.component.spec.ts
Normal file
189
src/app/components/favorites/favorites.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
100
src/app/components/favorites/favorites.component.ts
Normal file
100
src/app/components/favorites/favorites.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
149
src/app/components/files/files.component.html
Normal file
149
src/app/components/files/files.component.html
Normal 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>
|
427
src/app/components/files/files.component.spec.ts
Normal file
427
src/app/components/files/files.component.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
191
src/app/components/files/files.component.ts
Normal file
191
src/app/components/files/files.component.ts
Normal 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;
|
||||
}
|
||||
}
|
15
src/app/components/header/header.component.html
Normal file
15
src/app/components/header/header.component.html
Normal 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>
|
57
src/app/components/header/header.component.scss
Normal file
57
src/app/components/header/header.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
89
src/app/components/header/header.component.spec.ts
Normal file
89
src/app/components/header/header.component.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
49
src/app/components/header/header.component.ts
Normal file
49
src/app/components/header/header.component.ts
Normal 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;
|
||||
}
|
||||
}
|
15
src/app/components/layout/layout.component.html
Normal file
15
src/app/components/layout/layout.component.html
Normal 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>
|
8
src/app/components/layout/layout.component.scss
Normal file
8
src/app/components/layout/layout.component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
router-outlet {
|
||||
flex: 0 0;
|
||||
}
|
||||
}
|
104
src/app/components/layout/layout.component.spec.ts
Normal file
104
src/app/components/layout/layout.component.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
53
src/app/components/layout/layout.component.ts
Normal file
53
src/app/components/layout/layout.component.ts
Normal 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;
|
||||
}
|
||||
}
|
58
src/app/components/libraries/libraries.component.html
Normal file
58
src/app/components/libraries/libraries.component.html
Normal 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>
|
189
src/app/components/libraries/libraries.component.spec.ts
Normal file
189
src/app/components/libraries/libraries.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
80
src/app/components/libraries/libraries.component.ts
Normal file
80
src/app/components/libraries/libraries.component.ts
Normal 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
|
||||
}
|
||||
}
|
295
src/app/components/page.component.spec.ts
Normal file
295
src/app/components/page.component.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
124
src/app/components/page.component.ts
Normal file
124
src/app/components/page.component.ts
Normal 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;
|
||||
}
|
||||
}
|
3
src/app/components/preview/preview.component.html
Normal file
3
src/app/components/preview/preview.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<ng-container *ngIf="nodeId">
|
||||
<adf-viewer [fileNodeId]="nodeId"></adf-viewer>
|
||||
</ng-container>
|
4
src/app/components/preview/preview.component.scss
Normal file
4
src/app/components/preview/preview.component.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.app-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
56
src/app/components/preview/preview.component.ts
Normal file
56
src/app/components/preview/preview.component.ts
Normal 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])
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
122
src/app/components/recent-files/recent-files.component.html
Normal file
122
src/app/components/recent-files/recent-files.component.html
Normal 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>
|
152
src/app/components/recent-files/recent-files.component.spec.ts
Normal file
152
src/app/components/recent-files/recent-files.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
75
src/app/components/recent-files/recent-files.component.ts
Normal file
75
src/app/components/recent-files/recent-files.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
6
src/app/components/search/search.component.html
Normal file
6
src/app/components/search/search.component.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<adf-search-control
|
||||
[searchTerm]="searchTerm"
|
||||
[autocomplete]="false"
|
||||
[highlight]="true"
|
||||
(fileSelect)="onNodeClicked($event)">
|
||||
</adf-search-control>
|
9
src/app/components/search/search.component.scss
Normal file
9
src/app/components/search/search.component.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import './../../ui/variables';
|
||||
|
||||
adf-search-control {
|
||||
color: $alfresco-white;
|
||||
}
|
||||
|
||||
:host {
|
||||
height: $app-menu-height;
|
||||
}
|
71
src/app/components/search/search.component.spec.ts
Normal file
71
src/app/components/search/search.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
45
src/app/components/search/search.component.ts
Normal file
45
src/app/components/search/search.component.ts
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
130
src/app/components/shared-files/shared-files.component.html
Normal file
130
src/app/components/shared-files/shared-files.component.html
Normal 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>
|
161
src/app/components/shared-files/shared-files.component.spec.ts
Normal file
161
src/app/components/shared-files/shared-files.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
88
src/app/components/shared-files/shared-files.component.ts
Normal file
88
src/app/components/shared-files/shared-files.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
68
src/app/components/sidenav/sidenav.component.html
Normal file
68
src/app/components/sidenav/sidenav.component.html
Normal 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>
|
78
src/app/components/sidenav/sidenav.component.scss
Normal file
78
src/app/components/sidenav/sidenav.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
src/app/components/sidenav/sidenav.component.spec.ts
Normal file
85
src/app/components/sidenav/sidenav.component.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
105
src/app/components/sidenav/sidenav.component.ts
Normal file
105
src/app/components/sidenav/sidenav.component.ts
Normal 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;
|
||||
}
|
||||
}
|
89
src/app/components/trashcan/trashcan.component.html
Normal file
89
src/app/components/trashcan/trashcan.component.html
Normal 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>
|
77
src/app/components/trashcan/trashcan.component.spec.ts
Normal file
77
src/app/components/trashcan/trashcan.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
31
src/app/components/trashcan/trashcan.component.ts
Normal file
31
src/app/components/trashcan/trashcan.component.ts
Normal 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
68
src/app/ui/_layout.scss
Normal 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;
|
||||
}
|
||||
}
|
48
src/app/ui/_variables-color.scss
Normal file
48
src/app/ui/_variables-color.scss
Normal 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;
|
3
src/app/ui/_variables.scss
Normal file
3
src/app/ui/_variables.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import './_variables-color.scss';
|
||||
|
||||
$app-menu-height: 64px;
|
28
src/app/ui/application.scss
Normal file
28
src/app/ui/application.scss
Normal 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';
|
83
src/app/ui/overrides/_alfresco-document-list.scss
Normal file
83
src/app/ui/overrides/_alfresco-document-list.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
44
src/app/ui/overrides/_alfresco-upload-button.scss
Normal file
44
src/app/ui/overrides/_alfresco-upload-button.scss
Normal 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;
|
||||
}
|
||||
}
|
23
src/app/ui/overrides/_alfresco-upload-dialog.scss
Normal file
23
src/app/ui/overrides/_alfresco-upload-dialog.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
72
src/app/ui/overrides/_alfresco-upload-drag-area.scss
Normal file
72
src/app/ui/overrides/_alfresco-upload-drag-area.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
5
src/app/ui/overrides/_breadcrumb.scss
Normal file
5
src/app/ui/overrides/_breadcrumb.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@import '../variables';
|
||||
|
||||
.adf-breadcrumb {
|
||||
width: 0;
|
||||
}
|
36
src/app/ui/overrides/_toolbar.scss
Normal file
36
src/app/ui/overrides/_toolbar.scss
Normal 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
25
src/app/ui/theme.scss
Normal 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
201
src/assets/i18n/en.json
Normal 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
125
src/assets/i18n/ru.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
129
src/assets/images/alfresco-logo-white.svg
Normal file
129
src/assets/images/alfresco-logo-white.svg
Normal 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 |
@@ -1,6 +1,7 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
|
||||
@import '~ng2-alfresco-core/prebuilt-themes/adf-blue-orange.css';
|
||||
@import 'app/ui/application';
|
||||
|
||||
body, html {
|
||||
height: 100%;
|
Reference in New Issue
Block a user