diff --git a/cspell.json b/cspell.json index 9789a3c84..44f998e9e 100644 --- a/cspell.json +++ b/cspell.json @@ -21,6 +21,8 @@ "promisify", "xdescribe", "unfavorite", + "Snackbar", + "devtools", "unshare", "validators", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 46bf6cfe1..b0774cbd4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -35,6 +35,7 @@ import { ElectronModule } from '@ngstack/electron'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { AppComponent } from './app.component'; import { APP_ROUTES } from './app.routes'; @@ -77,6 +78,12 @@ import { SortingPreferenceKeyDirective } from './directives/sorting-preference-k import { INITIAL_STATE } from './store/states/app.state'; import { appReducer } from './store/reducers/app.reducer'; import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component'; +import { EditFolderDirective } from './directives/edit-folder.directive'; +import { SnackbarEffects } from './store/effects/snackbar.effects'; +import { NodeEffects } from './store/effects/node.effects'; +import { environment } from '../environments/environment'; +import { RouterEffects } from './store/effects/router.effects'; +import { CreateFolderDirective } from './directives/create-folder.directive'; @NgModule({ @@ -100,7 +107,8 @@ import { InfoDrawerComponent } from './components/info-drawer/info-drawer.compon StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), StoreRouterConnectingModule.forRoot({ stateKey: 'router' }), - EffectsModule.forRoot([]) + EffectsModule.forRoot([SnackbarEffects, NodeEffects, RouterEffects]), + !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 25 }) : [] ], declarations: [ AppComponent, @@ -132,7 +140,9 @@ import { InfoDrawerComponent } from './components/info-drawer/info-drawer.compon SearchComponent, SettingsComponent, SortingPreferenceKeyDirective, - InfoDrawerComponent + InfoDrawerComponent, + EditFolderDirective, + CreateFolderDirective ], providers: [ { provide: AppConfigService, useClass: HybridAppConfigService }, diff --git a/src/app/common/directives/delete-status.interface.ts b/src/app/common/directives/delete-status.interface.ts new file mode 100644 index 000000000..aa912b175 --- /dev/null +++ b/src/app/common/directives/delete-status.interface.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +export interface DeleteStatus { + success: any[]; + fail: any[]; + someFailed: boolean; + someSucceeded: boolean; + oneFailed: boolean; + oneSucceeded: boolean; + allSucceeded: boolean; + allFailed: boolean; + reset(): void; +} diff --git a/src/app/common/directives/deleted-node-info.interface.ts b/src/app/common/directives/deleted-node-info.interface.ts new file mode 100644 index 000000000..3e6a432f6 --- /dev/null +++ b/src/app/common/directives/deleted-node-info.interface.ts @@ -0,0 +1,30 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +export interface DeletedNodeInfo { + id: string; + name: string; + status: number; +} diff --git a/src/app/common/directives/node-copy.directive.ts b/src/app/common/directives/node-copy.directive.ts index 29911cba4..308403c3b 100644 --- a/src/app/common/directives/node-copy.directive.ts +++ b/src/app/common/directives/node-copy.directive.ts @@ -122,7 +122,7 @@ export class NodeCopyDirective { Observable.forkJoin(...batch) .subscribe( () => { - this.content.nodeDeleted.next(null); + this.content.nodesDeleted.next(null); }, (error) => { let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; diff --git a/src/app/common/directives/node-delete.directive.spec.ts b/src/app/common/directives/node-delete.directive.spec.ts index 16ad941af..15de3820c 100644 --- a/src/app/common/directives/node-delete.directive.spec.ts +++ b/src/app/common/directives/node-delete.directive.spec.ts @@ -23,17 +23,23 @@ * along with Alfresco. If not, see . */ -import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TranslationService, NodesApiService, NotificationService, CoreModule } from '@alfresco/adf-core'; +import { CoreModule, AlfrescoApiService } from '@alfresco/adf-core'; import { Component, DebugElement } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; import { NodeDeleteDirective } from './node-delete.directive'; import { ContentManagementService } from '../services/content-management.service'; -import { MatSnackBarModule } from '@angular/material'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { StoreModule } from '@ngrx/store'; +import { appReducer } from '../../store/reducers/app.reducer'; +import { INITIAL_STATE } from '../../store/states/app.state'; +import { EffectsModule, Actions, ofType } from '@ngrx/effects'; +import { NodeEffects } from '../../store/effects/node.effects'; +import { + SnackbarInfoAction, SNACKBAR_INFO, SNACKBAR_ERROR, + SnackbarErrorAction, SnackbarWarningAction, SNACKBAR_WARNING +} from '../../store/actions'; +import { map } from 'rxjs/operators'; @Component({ template: '
' @@ -46,20 +52,15 @@ describe('NodeDeleteDirective', () => { let component: TestComponent; let fixture: ComponentFixture; let element: DebugElement; - let notificationService: NotificationService; - let translationService: TranslationService; - let contentService: ContentManagementService; - let nodeApiService: NodesApiService; - let spySnackBar; + let alfrescoApiService: AlfrescoApiService; + let actions$: Actions; beforeEach(() => { TestBed.configureTestingModule({ imports: [ - BrowserAnimationsModule, - FormsModule, - ReactiveFormsModule, CoreModule, - MatSnackBarModule + StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), + EffectsModule.forRoot([NodeEffects]) ], declarations: [ NodeDeleteDirective, @@ -70,54 +71,62 @@ describe('NodeDeleteDirective', () => { ] }); + alfrescoApiService = TestBed.get(AlfrescoApiService); + alfrescoApiService.reset(); + + actions$ = TestBed.get(Actions); + 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('should raise info message on successful single file deletion', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null)); - it('notifies file deletion', () => { - spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null)); + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => { + done(); + }) + ); component.selection = [{ entry: { id: '1', name: 'name1' } }]; fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', 'APP.ACTIONS.UNDO', 10000 - ); - }); + tick(); + })); - it('notifies failed file deletion', () => { - spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.throw(null)); + it('should raise error message on failed single file deletion', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.reject(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); component.selection = [{ entry: { id: '1', name: 'name1' } }]; fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.NODE_DELETION', '', 10000 - ); - }); + tick(); + })); - it('notifies files deletion', () => { - spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null)); + it('should raise info message on successful multiple files deletion', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null)); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => { + done(); + }) + ); component.selection = [ { entry: { id: '1', name: 'name1' } }, @@ -127,13 +136,18 @@ describe('NodeDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL', 'APP.ACTIONS.UNDO', 10000 - ); - }); + tick(); + })); - it('notifies failed files deletion', () => { - spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.throw(null)); + it('should raise error message failed multiple files deletion', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.reject(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); component.selection = [ { entry: { id: '1', name: 'name1' } }, @@ -143,20 +157,25 @@ describe('NodeDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', '', 10000 - ); - }); + tick(); + })); - it('notifies partial deletion when only one file is successful', () => { - spyOn(nodeApiService, 'deleteNode').and.callFake((id) => { + it('should raise warning message when only one file is successful', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.callFake((id) => { if (id === '1') { - return Observable.throw(null); + return Promise.reject(null); } else { - return Observable.of(null); + return Promise.resolve(null); } }); + actions$.pipe( + ofType(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + component.selection = [ { entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } } @@ -165,26 +184,31 @@ describe('NodeDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', 'APP.ACTIONS.UNDO', 10000 - ); - }); + tick(); + })); - it('notifies partial deletion when some files are successful', () => { - spyOn(nodeApiService, 'deleteNode').and.callFake((id) => { + it('should raise warning message when some files are successfully deleted', fakeAsync(done => { + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.callFake((id) => { if (id === '1') { - return Observable.throw(null); + return Promise.reject(null); } if (id === '2') { - return Observable.of(null); + return Promise.resolve(null); } if (id === '3') { - return Observable.of(null); + return Promise.resolve(null); } }); + actions$.pipe( + ofType(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + component.selection = [ { entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }, @@ -194,23 +218,18 @@ describe('NodeDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(notificationService.openSnackMessageAction).toHaveBeenCalledWith( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', 'APP.ACTIONS.UNDO', 10000 - ); - }); + tick(); + })); }); + /* describe('Restore action', () => { beforeEach(() => { - spyOn(nodeApiService, 'deleteNode').and.returnValue(Observable.of(null)); - - spySnackBar = spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ - onAction: () => Observable.of({}) - }); + spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null)); }); it('notifies failed file on on restore', () => { - spyOn(nodeApiService, 'restoreNode').and.returnValue(Observable.throw(null)); + spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null)); component.selection = [ { entry: { id: '1', name: 'name1' } } @@ -224,7 +243,7 @@ describe('NodeDeleteDirective', () => { }); it('notifies failed files on on restore', () => { - spyOn(nodeApiService, 'restoreNode').and.returnValue(Observable.throw(null)); + spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null)); component.selection = [ { entry: { id: '1', name: 'name1' } }, @@ -240,11 +259,11 @@ describe('NodeDeleteDirective', () => { it('signals files restored', () => { spyOn(contentService.nodeRestored, 'next'); - spyOn(nodeApiService, 'restoreNode').and.callFake((id) => { + spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.callFake((id) => { if (id === '1') { - return Observable.of(null); + return Promise.resolve(null); } else { - return Observable.throw(null); + return Promise.reject(null); } }); @@ -259,4 +278,5 @@ describe('NodeDeleteDirective', () => { expect(contentService.nodeRestored.next).toHaveBeenCalled(); }); }); + */ }); diff --git a/src/app/common/directives/node-delete.directive.ts b/src/app/common/directives/node-delete.directive.ts index bce80a8c9..d87279dab 100644 --- a/src/app/common/directives/node-delete.directive.ts +++ b/src/app/common/directives/node-delete.directive.ts @@ -24,218 +24,35 @@ */ import { Directive, HostListener, Input } from '@angular/core'; - -import { TranslationService, NodesApiService, NotificationService } from '@alfresco/adf-core'; import { MinimalNodeEntity } from 'alfresco-js-api'; -import { Observable } from 'rxjs/Rx'; - -import { ContentManagementService } from '../services/content-management.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { DeleteNodesAction, NodeInfo } from '../../store/actions'; @Directive({ selector: '[acaDeleteNode]' }) export class NodeDeleteDirective { - static RESTORE_MESSAGE_DURATION = 3000; - static DELETE_MESSAGE_DURATION = 10000; // tslint:disable-next-line:no-input-rename @Input('acaDeleteNode') selection: MinimalNodeEntity[]; + constructor(private store: Store) {} + @HostListener('click') onClick() { - this.deleteSelected(); - } + if (this.selection && this.selection.length > 0) { + const toDelete: NodeInfo[] = this.selection.map(node => { + const { name } = node.entry; + const id = node.entry.nodeId || node.entry.id; - 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); - const message = this.getDeleteMessage(processedData); - const withUndo = processedData.someSucceeded ? this.translation.instant('APP.ACTIONS.UNDO') : ''; - - this.notification.openSnackMessageAction(message, withUndo, NodeDeleteDirective.DELETE_MESSAGE_DURATION) - .onAction() - .subscribe(() => this.restore(processedData.success)); - - if (processedData.someSucceeded) { - this.content.nodeDeleted.next(null); - } - } - ); - } - - private restore(items: any[]): 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) { - const message = this.getRestoreMessage(processedData); - this.notification.openSnackMessageAction( - message, '' , NodeDeleteDirective.RESTORE_MESSAGE_DURATION - ); - } - - if (processedData.someSucceeded) { - this.content.nodeRestored.next(null); - } - } - ); - } - - private performAction(action: string, item: any): Observable { - const { name } = item; - // Check if there's nodeId for Shared Files - const id = item.nodeId || item.id; - - let performedAction: any = null; - - if (action === 'delete') { - performedAction = this.nodesApi.deleteNode(id); - } else { - performedAction = this.nodesApi.restoreNode(id); - } - - return performedAction - .map(() => { return { id, - name, - status: 1 + name }; - }) - .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): string { - if (status.someFailed && !status.oneFailed) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', - { number: status.failed.length } - ); - } - - if (status.oneFailed) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.NODE_RESTORE', - { name: status.failed[0].name } - ); - } - } - - private getDeleteMessage(status): string { - if (status.allFailed && !status.oneFailed) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', - { number: status.failed.length } - ); - } - - if (status.allSucceeded && !status.oneSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL', - { number: status.success.length } - ); - } - - if (status.someFailed && status.someSucceeded && !status.oneSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', - { - success: status.success.length, - failed: status.failed.length - } - ); - } - - if (status.someFailed && status.oneSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', - { - success: status.success.length, - failed: status.failed.length - } - ); - } - - if (status.oneFailed && !status.someSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.NODE_DELETION', - { name: status.failed[0].name } - ); - } - - if (status.oneSucceeded && !status.someFailed) { - return this.translation.instant( - 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', - { name: status.success[0].name } - ); + this.store.dispatch(new DeleteNodesAction(toDelete)); } } } diff --git a/src/app/common/directives/node-move.directive.spec.ts b/src/app/common/directives/node-move.directive.spec.ts index cb2ac5b15..342abe7ed 100644 --- a/src/app/common/directives/node-move.directive.spec.ts +++ b/src/app/common/directives/node-move.directive.spec.ts @@ -24,7 +24,7 @@ */ import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Observable } from 'rxjs/Rx'; import { TranslationService, NodesApiService, NotificationService, CoreModule } from '@alfresco/adf-core'; @@ -34,6 +34,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NodeActionsService } from '../services/node-actions.service'; import { NodeMoveDirective } from './node-move.directive'; import { ContentManagementService } from '../services/content-management.service'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule, Actions, ofType } from '@ngrx/effects'; +import { appReducer } from '../../store/reducers/app.reducer'; +import { NodeEffects } from '../../store/effects/node.effects'; +import { INITIAL_STATE } from '../../store/states/app.state'; +import { SnackbarErrorAction, SNACKBAR_ERROR } from '../../store/actions'; +import { map } from 'rxjs/operators'; @Component({ template: '
' @@ -50,12 +57,15 @@ describe('NodeMoveDirective', () => { let nodesApiService: NodesApiService; let service: NodeActionsService; let translationService: TranslationService; + let actions$: Actions; beforeEach(() => { TestBed.configureTestingModule({ imports: [ BrowserAnimationsModule, - CoreModule + CoreModule, + StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), + EffectsModule.forRoot([NodeEffects]) ], declarations: [ NodeMoveDirective, @@ -68,6 +78,7 @@ describe('NodeMoveDirective', () => { ] }); + actions$ = TestBed.get(Actions); fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; element = fixture.debugElement.query(By.directive(NodeMoveDirective)); @@ -407,9 +418,14 @@ describe('NodeMoveDirective', () => { .toHaveBeenCalledWith('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR', 'APP.ACTIONS.UNDO', 10000); }); - it('should notify when error occurs on Undo Move action', () => { + it('should notify when error occurs on Undo Move action', fakeAsync(done => { spyOn(nodesApiService, 'restoreNode').and.returnValue(Observable.throw(null)); + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + const initialParent = 'parent-id-0'; const node = { entry: { id: 'node-to-move-id', name: 'conflicting-name', parentId: initialParent } }; component.selection = [node]; @@ -429,15 +445,16 @@ describe('NodeMoveDirective', () => { service.contentMoved.next(movedItems); expect(nodesApiService.restoreNode).toHaveBeenCalled(); - expect(notificationService.openSnackMessageAction) - .toHaveBeenCalledWith('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR', 'APP.ACTIONS.UNDO', 10000); - expect(notificationService.openSnackMessage) - .toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC', 3000); - }); + })); - it('should notify when some error of type Error occurs on Undo Move action', () => { + it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(done => { spyOn(nodesApiService, 'restoreNode').and.returnValue(Observable.throw(new Error('oops!'))); + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + const initialParent = 'parent-id-0'; const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; component.selection = [ node ]; @@ -456,15 +473,16 @@ describe('NodeMoveDirective', () => { service.contentMoved.next(movedItems); expect(nodesApiService.restoreNode).toHaveBeenCalled(); - expect(notificationService.openSnackMessageAction) - .toHaveBeenCalledWith('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR', 'APP.ACTIONS.UNDO', 10000); - expect(notificationService.openSnackMessage) - .toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC', 3000); - }); + })); - it('should notify permission error when it occurs on Undo Move action', () => { + it('should notify permission error when it occurs on Undo Move action', fakeAsync(done => { spyOn(nodesApiService, 'restoreNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + const initialParent = 'parent-id-0'; const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; component.selection = [ node ]; @@ -484,11 +502,7 @@ describe('NodeMoveDirective', () => { expect(service.moveNodes).toHaveBeenCalled(); expect(nodesApiService.restoreNode).toHaveBeenCalled(); - expect(notificationService.openSnackMessageAction) - .toHaveBeenCalledWith('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR', 'APP.ACTIONS.UNDO', 10000); - expect(notificationService.openSnackMessage) - .toHaveBeenCalledWith('APP.MESSAGES.ERRORS.PERMISSION', 3000); - }); + })); }); }); diff --git a/src/app/common/directives/node-move.directive.ts b/src/app/common/directives/node-move.directive.ts index d2d6a91a0..2c7914f69 100644 --- a/src/app/common/directives/node-move.directive.ts +++ b/src/app/common/directives/node-move.directive.ts @@ -31,6 +31,9 @@ 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'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { SnackbarErrorAction } from '../../store/actions'; @Directive({ selector: '[acaMoveNode]' @@ -47,6 +50,7 @@ export class NodeMoveDirective { } constructor( + private store: Store, private content: ContentManagementService, private notification: NotificationService, private nodeActionsService: NodeActionsService, @@ -65,7 +69,7 @@ export class NodeMoveDirective { const [ operationResult, moveResponse ] = result; this.toastMessage(operationResult, moveResponse); - this.content.nodeMoved.next(null); + this.content.nodesMoved.next(null); }, (error) => { this.toastMessage(error); @@ -192,24 +196,21 @@ export class NodeMoveDirective { }) .subscribe( () => { - this.content.nodeMoved.next(null); + this.content.nodesMoved.next(null); }, - (error) => { - - let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; + error => { + let message = 'APP.MESSAGES.ERRORS.GENERIC'; let errorJson = null; try { errorJson = JSON.parse(error.message); - } catch (e) { // - } + } catch {} if (errorJson && errorJson.error && errorJson.error.statusCode === 403) { - i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; + message = 'APP.MESSAGES.ERRORS.PERMISSION'; } - const message = this.translation.instant(i18nMessageString); - this.notification.openSnackMessage(message, NodeActionsService.SNACK_MESSAGE_DURATION); + this.store.dispatch(new SnackbarErrorAction(message)); } ); } diff --git a/src/app/common/directives/node-permanent-delete.directive.spec.ts b/src/app/common/directives/node-permanent-delete.directive.spec.ts index 989f78f79..db10f1329 100644 --- a/src/app/common/directives/node-permanent-delete.directive.spec.ts +++ b/src/app/common/directives/node-permanent-delete.directive.spec.ts @@ -27,14 +27,25 @@ 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 { AlfrescoApiService, TranslationService, NotificationService, CoreModule } from '@alfresco/adf-core'; +import { AlfrescoApiService, CoreModule } from '@alfresco/adf-core'; import { NodePermanentDeleteDirective } from './node-permanent-delete.directive'; import { MatDialogModule, MatDialog } from '@angular/material'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule } from '@ngrx/store'; +import { INITIAL_STATE } from '../../store/states/app.state'; +import { appReducer } from '../../store/reducers/app.reducer'; +import { Actions, ofType, EffectsModule } from '@ngrx/effects'; +import { + SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction, + SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING +} from '../../store/actions'; +import { map } from 'rxjs/operators'; +import { NodeEffects } from '../../store/effects/node.effects'; +import { ContentManagementService } from '../services/content-management.service'; @Component({ - template: `
` + template: `
` }) class TestComponent { selection = []; @@ -44,47 +55,44 @@ describe('NodePermanentDeleteDirective', () => { let fixture: ComponentFixture; let element: DebugElement; let component: TestComponent; - let alfrescoService: AlfrescoApiService; - let translation: TranslationService; - let notificationService: NotificationService; - let nodesService; - let directiveInstance; + let alfrescoApiService: AlfrescoApiService; let dialog: MatDialog; + let actions$: Actions; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - CoreModule, - MatDialogModule + CoreModule.forRoot(), + MatDialogModule, + StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), + EffectsModule.forRoot([NodeEffects]) ], declarations: [ NodePermanentDeleteDirective, TestComponent + ], + providers: [ + ContentManagementService ] }) .compileComponents() .then(() => { + alfrescoApiService = TestBed.get(AlfrescoApiService); + alfrescoApiService.reset(); + + actions$ = TestBed.get(Actions); + fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective)); - directiveInstance = element.injector.get(NodePermanentDeleteDirective); dialog = TestBed.get(MatDialog); - - alfrescoService = TestBed.get(AlfrescoApiService); - alfrescoService.reset(); - - translation = TestBed.get(TranslationService); - notificationService = TestBed.get(NotificationService); }); })); beforeEach(() => { - nodesService = alfrescoService.getInstance().nodes; - - spyOn(translation, 'instant').and.returnValue(Observable.of('message')); - spyOn(notificationService, 'openSnackMessage').and.returnValue({}); spyOn(dialog, 'open').and.returnValue({ afterClosed() { @@ -94,18 +102,18 @@ describe('NodePermanentDeleteDirective', () => { }); it('does not purge nodes if no selection', () => { - spyOn(nodesService, 'purgeDeletedNode'); + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode'); component.selection = []; fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(nodesService.purgeDeletedNode).not.toHaveBeenCalled(); + expect(alfrescoApiService.nodesApi.purgeDeletedNode).not.toHaveBeenCalled(); }); it('call purge nodes if selection is not empty', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.resolve()); + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.returnValue(Promise.resolve()); component.selection = [ { entry: { id: '1' } } ]; @@ -113,12 +121,19 @@ describe('NodePermanentDeleteDirective', () => { element.triggerEventHandler('click', null); tick(); - expect(nodesService.purgeDeletedNode).toHaveBeenCalled(); + expect(alfrescoApiService.nodesApi.purgeDeletedNode).toHaveBeenCalled(); })); describe('notification', () => { - it('notifies on multiple fail and one success', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => { + it('raises warning on multiple fail and one success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_WARNING), + map((action: SnackbarWarningAction) => { + done(); + }) + ); + + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.callFake((id) => { if (id === '1') { return Promise.resolve(); } @@ -141,16 +156,17 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).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) => { + it('raises warning on multiple success and multiple fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_WARNING), + map((action: SnackbarWarningAction) => { + done(); + }) + ); + + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.callFake((id) => { if (id === '1') { return Promise.resolve(); } @@ -178,16 +194,17 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).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()); + it('raises info on one selected node success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.returnValue(Promise.resolve()); component.selection = [ { entry: { id: '1', name: 'name1' } } @@ -196,16 +213,17 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).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({})); + it('raises error on one selected node fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.returnValue(Promise.reject({})); component.selection = [ { entry: { id: '1', name: 'name1' } } @@ -214,16 +232,16 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.SINGULAR', - { name: 'name1' } - ); })); - it('notifies on selected nodes success', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => { + it('raises info on all nodes success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.callFake((id) => { if (id === '1') { return Promise.resolve(); } @@ -241,16 +259,16 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PLURAL', - { number: 2 } - ); })); - it('notifies on selected nodes fail', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => { + it('raises error on all nodes fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + spyOn(alfrescoApiService.nodesApi, 'purgeDeletedNode').and.callFake((id) => { if (id === '1') { return Promise.reject({}); } @@ -268,96 +286,6 @@ describe('NodePermanentDeleteDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(notificationService.openSnackMessage).toHaveBeenCalled(); - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.PLURAL', - { number: 2 } - ); - })); - }); - - describe('refresh()', () => { - it('resets selection on success', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.resolve()); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(directiveInstance.selection).toEqual([]); - })); - - it('resets selection on error', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.reject({})); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(directiveInstance.selection).toEqual([]); - })); - - it('resets status', fakeAsync(() => { - const status = directiveInstance.processStatus([ - { status: 0 }, - { status: 1 } - ]); - - expect(status.fail.length).toBe(1); - expect(status.success.length).toBe(1); - - status.reset(); - - expect(status.fail.length).toBe(0); - expect(status.success.length).toBe(0); - })); - - it('dispatch event on partial success', fakeAsync(() => { - spyOn(element.nativeElement, 'dispatchEvent'); - spyOn(nodesService, 'purgeDeletedNode').and.callFake((id) => { - if (id === '1') { - return Promise.reject({}); - } - - if (id === '2') { - return Promise.resolve(); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(element.nativeElement.dispatchEvent).toHaveBeenCalled(); - })); - - it('does not dispatch event on error', fakeAsync(() => { - spyOn(nodesService, 'purgeDeletedNode').and.returnValue(Promise.reject({})); - spyOn(element.nativeElement, 'dispatchEvent'); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(element.nativeElement.dispatchEvent).not.toHaveBeenCalled(); })); }); }); diff --git a/src/app/common/directives/node-permanent-delete.directive.ts b/src/app/common/directives/node-permanent-delete.directive.ts index 501976ae1..17eaf92c6 100644 --- a/src/app/common/directives/node-permanent-delete.directive.ts +++ b/src/app/common/directives/node-permanent-delete.directive.ts @@ -23,24 +23,29 @@ * along with Alfresco. If not, see . */ -import { Directive, ElementRef, HostListener, Input } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; - -import { TranslationService, AlfrescoApiService, NotificationService } from '@alfresco/adf-core'; +import { Directive, HostListener, Input } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { MatDialog } from '@angular/material'; import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; +import { Store } from '@ngrx/store'; + +import { AppStore } from '../../store/states/app.state'; +import { NodeInfo, PurgeDeletedNodesAction } from '../../store/actions'; @Directive({ - // tslint:disable-next-line:directive-selector - selector: '[app-permanent-delete-node]' + selector: '[acaPermanentDelete]' }) export class NodePermanentDeleteDirective { // tslint:disable-next-line:no-input-rename - @Input('app-permanent-delete-node') + @Input('acaPermanentDelete') selection: MinimalNodeEntity[]; + constructor( + private store: Store, + private dialog: MatDialog + ) {} + @HostListener('click') onClick() { const dialogRef = this.dialog.open(ConfirmDialogComponent, { @@ -55,165 +60,17 @@ export class NodePermanentDeleteDirective { dialogRef.afterClosed().subscribe(result => { if (result === true) { - this.purge(); + const nodesToDelete: NodeInfo[] = this.selection.map(node => { + const { name } = node.entry; + const id = node.entry.nodeId || node.entry.id; + + return { + id, + name + }; + }); + this.store.dispatch(new PurgeDeletedNodesAction(nodesToDelete)); } }); } - - constructor( - private alfrescoApiService: AlfrescoApiService, - private translation: TranslationService, - private notification: NotificationService, - private el: ElementRef, - private dialog: MatDialog - ) {} - - private purge() { - if (!this.selection.length) { - return; - } - - const batch = this.getPurgedNodesBatch(this.selection); - - Observable.forkJoin(batch) - .subscribe( - (purgedNodes) => { - const status = this.processStatus(purgedNodes); - - this.purgeNotification(status); - - if (status.success.length) { - this.emitDone(); - } - - this.selection = []; - status.reset(); - } - ); - } - - private getPurgedNodesBatch(selection): Observable { - return selection.map((node: MinimalNodeEntity) => this.purgeDeletedNode(node)); - } - - private purgeDeletedNode(node): Observable { - const { id, name } = node.entry; - const promise = this.alfrescoApiService.getInstance().nodes.purgeDeletedNode(id); - - return Observable.from(promise) - .map(() => ({ - status: 1, - id, - name - })) - .catch((error) => { - return Observable.of({ - status: 0, - id, - name - }); - }); - } - - private purgeNotification(status): void { - const message = this.getPurgeMessage(status); - this.notification.openSnackMessage(message, 3000); - } - - private getPurgeMessage(status): string { - if (status.oneSucceeded && status.someFailed && !status.oneFailed) { - return this.translation.instant( - '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.instant( - 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_PLURAL', - { - number: status.success.length, - failed: status.fail.length - } - ); - } - - if (status.oneSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.SINGULAR', - { name: status.success[0].name } - ); - } - - if (status.oneFailed) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.SINGULAR', - { name: status.fail[0].name } - ); - } - - if (status.allSucceeded) { - return this.translation.instant( - 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PLURAL', - { number: status.success.length } - ); - } - - if (status.allFailed) { - return this.translation.instant( - 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.PLURAL', - { number: status.fail.length } - ); - } - } - - private processStatus(data = []): any { - const status = { - fail: [], - success: [], - get someFailed() { - return !!(this.fail.length); - }, - get someSucceeded() { - return !!(this.success.length); - }, - get oneFailed() { - return this.fail.length === 1; - }, - get oneSucceeded() { - return this.success.length === 1; - }, - get allSucceeded() { - return this.someSucceeded && !this.someFailed; - }, - get allFailed() { - return this.someFailed && !this.someSucceeded; - }, - reset() { - this.fail = []; - this.success = []; - } - }; - - return data.reduce( - (acc, node) => { - if (node.status) { - acc.success.push(node); - } else { - acc.fail.push(node); - } - - return acc; - }, - status - ); - } - - private emitDone() { - const e = new CustomEvent('selection-node-deleted', { bubbles: true }); - this.el.nativeElement.dispatchEvent(e); - } } diff --git a/src/app/common/directives/node-restore.directive.spec.ts b/src/app/common/directives/node-restore.directive.spec.ts index 2b9af8b3b..403e28254 100644 --- a/src/app/common/directives/node-restore.directive.spec.ts +++ b/src/app/common/directives/node-restore.directive.spec.ts @@ -24,16 +24,24 @@ */ 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 { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Observable } from 'rxjs/Rx'; -import { AlfrescoApiService, TranslationService, NotificationService, CoreModule } from '@alfresco/adf-core'; +import { AlfrescoApiService, TranslationService, CoreModule } from '@alfresco/adf-core'; import { NodeRestoreDirective } from './node-restore.directive'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ContentManagementService } from '../services/content-management.service'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule, Actions, ofType } from '@ngrx/effects'; +import { appReducer } from '../../store/reducers/app.reducer'; +import { INITIAL_STATE } from '../../store/states/app.state'; +import { SnackbarErrorAction, + SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO, + NavigateRouteAction, NAVIGATE_ROUTE } from '../../store/actions'; +import { map } from 'rxjs/operators'; @Component({ template: `
` @@ -48,73 +56,70 @@ describe('NodeRestoreDirective', () => { let component: TestComponent; let alfrescoService: AlfrescoApiService; let translation: TranslationService; - let notificationService: NotificationService; - let router: Router; - let nodesService; - let coreApi; - let directiveInstance; + let directiveInstance: NodeRestoreDirective; + let contentManagementService: ContentManagementService; + let actions$: Actions; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, RouterTestingModule, - CoreModule + CoreModule.forRoot(), + StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), + EffectsModule.forRoot([]) ], declarations: [ NodeRestoreDirective, TestComponent + ], + providers: [ + ContentManagementService ] - }) - .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); - alfrescoService.reset(); - - translation = TestBed.get(TranslationService); - notificationService = TestBed.get(NotificationService); - router = TestBed.get(Router); }); - })); - beforeEach(() => { - nodesService = alfrescoService.getInstance().nodes; - coreApi = alfrescoService.getInstance().core; + actions$ = TestBed.get(Actions); + alfrescoService = TestBed.get(AlfrescoApiService); + alfrescoService.reset(); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + element = fixture.debugElement.query(By.directive(NodeRestoreDirective)); + directiveInstance = element.injector.get(NodeRestoreDirective); + + translation = TestBed.get(TranslationService); spyOn(translation, 'instant').and.returnValue(Observable.of('message')); + + contentManagementService = TestBed.get(ContentManagementService); }); it('does not restore nodes if no selection', () => { - spyOn(nodesService, 'restoreNode'); + spyOn(alfrescoService.nodesApi, 'restoreNode'); component.selection = []; fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(nodesService.restoreNode).not.toHaveBeenCalled(); + expect(alfrescoService.nodesApi.restoreNode).not.toHaveBeenCalled(); }); it('does not restore nodes if selection has nodes without path', () => { - spyOn(nodesService, 'restoreNode'); + spyOn(alfrescoService.nodesApi, 'restoreNode'); component.selection = [ { entry: { id: '1' } } ]; fixture.detectChanges(); element.triggerEventHandler('click', null); - expect(nodesService.restoreNode).not.toHaveBeenCalled(); + expect(alfrescoService.nodesApi.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({ + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.resolve()); + spyOn(alfrescoService.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({ list: { entries: [] } })); @@ -124,70 +129,43 @@ describe('NodeRestoreDirective', () => { element.triggerEventHandler('click', null); tick(); - expect(nodesService.restoreNode).toHaveBeenCalled(); + expect(alfrescoService.nodesApi.restoreNode).toHaveBeenCalled(); })); describe('refresh()', () => { - it('reset selection', fakeAsync(() => { + it('dispatch event on finish', fakeAsync(done => { spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null); - spyOn(nodesService, 'restoreNode').and.returnValue(Promise.resolve()); - spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({ + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.resolve()); + spyOn(alfrescoService.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(); + contentManagementService.nodesRestored.subscribe(() => done()); })); }); describe('notification', () => { beforeEach(() => { - spyOn(coreApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({ + spyOn(alfrescoService.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve({ list: { entries: [] } })); }); - it('notifies on partial multiple fail ', fakeAsync(() => { + it('should raise error message on partial multiple fail ', fakeAsync(done => { const error = { message: '{ "error": {} }' }; - spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) }); + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); - spyOn(nodesService, 'restoreNode').and.callFake((id) => { + spyOn(alfrescoService.nodesApi, 'restoreNode').and.callFake((id) => { if (id === '1') { return Promise.resolve(); } @@ -210,18 +188,16 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL', - { number: 2 } - ); })); - it('notifies fail when restored node exist, error 409', fakeAsync(() => { + it('should raise error message when restored node exist, error 409', fakeAsync(done => { const error = { message: '{ "error": { "statusCode": 409 } }' }; + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(error)); - spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) }); - spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error)); + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); component.selection = [ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } } @@ -230,18 +206,17 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS', - { name: 'name1' } - ); })); - it('notifies fail when restored node returns different statusCode', fakeAsync(() => { + it('should raise error message when restored node returns different statusCode', fakeAsync(done => { const error = { message: '{ "error": { "statusCode": 404 } }' }; - spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) }); - spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error)); + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(error)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); component.selection = [ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } } @@ -250,18 +225,17 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).toHaveBeenCalledWith( - 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC', - { name: 'name1' } - ); })); - it('notifies fail when restored node location is missing', fakeAsync(() => { + it('should raise error message when restored node location is missing', fakeAsync(done => { const error = { message: '{ "error": { } }' }; - spyOn(notificationService, 'openSnackMessageAction').and.returnValue({ onAction: () => Observable.throw(null) }); - spyOn(nodesService, 'restoreNode').and.returnValue(Promise.reject(error)); + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(error)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); component.selection = [ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } } @@ -270,16 +244,10 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).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) => { + it('should raise info message when restore multiple nodes', fakeAsync(done => { + spyOn(alfrescoService.nodesApi, 'restoreNode').and.callFake((id) => { if (id === '1') { return Promise.resolve(); } @@ -289,6 +257,11 @@ describe('NodeRestoreDirective', () => { } }); + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => done()) + ); + component.selection = [ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }, { entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } } @@ -297,15 +270,15 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).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()); + xit('should raise info message when restore selected node', fakeAsync(done => { + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.resolve()); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => done()) + ); component.selection = [ { entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } } @@ -314,17 +287,15 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(translation.instant).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({}) }); + it('navigate to restore selected node location onAction', fakeAsync(done => { + spyOn(alfrescoService.nodesApi, 'restoreNode').and.returnValue(Promise.resolve()); + + actions$.pipe( + ofType(NAVIGATE_ROUTE), + map(action => done()) + ); component.selection = [ { @@ -341,8 +312,6 @@ describe('NodeRestoreDirective', () => { fixture.detectChanges(); element.triggerEventHandler('click', null); tick(); - - expect(router.navigate).toHaveBeenCalled(); })); }); }); diff --git a/src/app/common/directives/node-restore.directive.ts b/src/app/common/directives/node-restore.directive.ts index cb0ff38b3..a71d9b15a 100644 --- a/src/app/common/directives/node-restore.directive.ts +++ b/src/app/common/directives/node-restore.directive.ts @@ -23,22 +23,34 @@ * along with Alfresco. If not, see . */ -import { Directive, ElementRef, HostListener, Input } from '@angular/core'; -import { Router } from '@angular/router'; +import { Directive, HostListener, Input } from '@angular/core'; import { Observable } from 'rxjs/Rx'; -import { TranslationService, AlfrescoApiService, NotificationService } from '@alfresco/adf-core'; -import { MinimalNodeEntity, PathInfoEntity, DeletedNodesPaging } from 'alfresco-js-api'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { + MinimalNodeEntity, + PathInfoEntity, + DeletedNodesPaging +} from 'alfresco-js-api'; +import { DeletedNodeInfo } from './deleted-node-info.interface'; +import { DeleteStatus } from './delete-status.interface'; +import { ContentManagementService } from '../services/content-management.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { + NavigateRouteAction, + SnackbarAction, + SnackbarErrorAction, + SnackbarInfoAction, + SnackbarUserAction +} from '../../store/actions'; @Directive({ selector: '[acaRestoreNode]' }) export class NodeRestoreDirective { - private restoreProcessStatus; - // tslint:disable-next-line:no-input-rename - @Input('acaRestoreNode') - selection: MinimalNodeEntity[]; + @Input('acaRestoreNode') selection: MinimalNodeEntity[]; @HostListener('click') onClick() { @@ -46,81 +58,69 @@ export class NodeRestoreDirective { } constructor( + private store: Store, private alfrescoApiService: AlfrescoApiService, - private translation: TranslationService, - private router: Router, - private notification: NotificationService, - private el: ElementRef - ) { - this.restoreProcessStatus = this.processStatus(); - } + private contentManagementService: ContentManagementService + ) {} - private restore(selection: any) { + private restore(selection: MinimalNodeEntity[] = []) { if (!selection.length) { return; } - const nodesWithPath = this.getNodesWithPath(selection); + const nodesWithPath = selection.filter(node => node.entry.path); if (selection.length && !nodesWithPath.length) { - this.restoreProcessStatus.fail.push(...selection); - this.restoreNotification(); + const failedStatus = this.processStatus([]); + failedStatus.fail.push(...selection); + this.restoreNotification(failedStatus); this.refresh(); return; } - this.restoreNodesBatch(nodesWithPath) - .do((restoredNodes) => { - const status = this.processStatus(restoredNodes); + let status: DeleteStatus; - this.restoreProcessStatus.fail.push(...status.fail); - this.restoreProcessStatus.success.push(...status.success); + Observable.forkJoin(nodesWithPath.map(node => this.restoreNode(node))) + .do(restoredNodes => { + status = this.processStatus(restoredNodes); }) .flatMap(() => this.getDeletedNodes()) - .subscribe( - (deletedNodesList: DeletedNodesPaging) => { - const { entries: nodeList } = deletedNodesList.list; - const { fail: restoreErrorNodes } = this.restoreProcessStatus; - const selectedNodes = this.diff(restoreErrorNodes, selection, false); - const remainingNodes = this.diff(selectedNodes, nodeList); + .subscribe((nodes: DeletedNodesPaging) => { + const selectedNodes = this.diff(status.fail, selection, false); + const remainingNodes = this.diff( + selectedNodes, + nodes.list.entries + ); - if (!remainingNodes.length) { - this.restoreNotification(); - this.refresh(); - } else { - this.restore(remainingNodes); - } + if (!remainingNodes.length) { + this.restoreNotification(status); + this.refresh(); + } else { + this.restore(remainingNodes); } - ); - } - - private restoreNodesBatch(batch: MinimalNodeEntity[]): Observable { - return Observable.forkJoin(batch.map((node) => this.restoreNode(node))); - } - - private getNodesWithPath(selection): MinimalNodeEntity[] { - return selection.filter((node) => node.entry.path); + }); } private getDeletedNodes(): Observable { - const promise = this.alfrescoApiService.getInstance() - .core.nodesApi.getDeletedNodes({ include: [ 'path' ] }); - - return Observable.from(promise); + return Observable.from( + this.alfrescoApiService.nodesApi.getDeletedNodes({ + include: ['path'] + }) + ); } - private restoreNode(node): Observable { + private restoreNode(node: MinimalNodeEntity): Observable { const { entry } = node; - const promise = this.alfrescoApiService.getInstance().nodes.restoreNode(entry.id); - - return Observable.from(promise) + return Observable.from( + this.alfrescoApiService.nodesApi.restoreNode(entry.id) + ) .map(() => ({ status: 1, entry })) - .catch((error) => { - const { statusCode } = (JSON.parse(error.message)).error; + .catch(error => { + const { statusCode } = JSON.parse(error.message).error; return Observable.of({ status: 0, @@ -130,13 +130,7 @@ export class NodeRestoreDirective { }); } - 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 { + private diff(selection, list, fromList = true): any { const ids = selection.map(item => item.entry.id); return list.filter(item => { @@ -148,15 +142,15 @@ export class NodeRestoreDirective { }); } - private processStatus(data = []): any { + private processStatus(data: DeletedNodeInfo[] = []): DeleteStatus { const status = { fail: [], success: [], get someFailed() { - return !!(this.fail.length); + return !!this.fail.length; }, get someSucceeded() { - return !!(this.success.length); + return !!this.success.length; }, get oneFailed() { return this.fail.length === 1; @@ -176,91 +170,85 @@ export class NodeRestoreDirective { } }; - return data.reduce( - (acc, node) => { - if (node.status) { - acc.success.push(node); - } else { - acc.fail.push(node); - } + return data.reduce((acc, node) => { + if (node.status) { + acc.success.push(node); + } else { + acc.fail.push(node); + } - return acc; - }, - status - ); + return acc; + }, status); } - private getRestoreMessage(): string { - const { restoreProcessStatus: status } = this; - + private getRestoreMessage(status: DeleteStatus): SnackbarAction { if (status.someFailed && !status.oneFailed) { - return this.translation.instant( + return new SnackbarErrorAction( 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL', - { - number: status.fail.length - } + { number: status.fail.length } ); } if (status.oneFailed && status.fail[0].statusCode) { if (status.fail[0].statusCode === 409) { - return this.translation.instant( + return new SnackbarErrorAction( 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS', - { - name: status.fail[0].entry.name - } + { name: status.fail[0].entry.name } ); } else { - return this.translation.instant( + return new SnackbarErrorAction( 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC', - { - name: status.fail[0].entry.name - } + { name: status.fail[0].entry.name } ); } } if (status.oneFailed && !status.fail[0].statusCode) { - return this.translation.instant( + return new SnackbarErrorAction( 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING', - { - name: status.fail[0].entry.name - } + { name: status.fail[0].entry.name } ); } if (status.allSucceeded && !status.oneSucceeded) { - return this.translation.instant('APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL'); + return new SnackbarInfoAction( + 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL' + ); } if (status.allSucceeded && status.oneSucceeded) { - return this.translation.instant( + return new SnackbarInfoAction( 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR', - { - name: status.success[0].entry.name - } + { name: status.success[0].entry.name } ); } + + return null; + } + + restoreNotification(status: DeleteStatus): void { + const message = this.getRestoreMessage(status); + + if (message) { + if (status.oneSucceeded && !status.someFailed) { + const path: PathInfoEntity = status.success[0].entry.path; + const parent = path.elements[path.elements.length - 1]; + const navigate = new NavigateRouteAction([ + '/personal-files', + parent.id + ]); + + message.userAction = new SnackbarUserAction( + 'APP.ACTIONS.VIEW', + navigate + ); + } + + this.store.dispatch(message); + } } - private restoreNotification(): void { - const status = Object.assign({}, this.restoreProcessStatus); - const action = (status.oneSucceeded && !status.someFailed) ? this.translation.translate.instant('APP.ACTIONS.VIEW') : ''; - const message = this.getRestoreMessage(); - - 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); + this.contentManagementService.nodesRestored.next(); } } diff --git a/src/app/common/directives/node-versions.directive.ts b/src/app/common/directives/node-versions.directive.ts index 367eb31c4..49c7ee484 100644 --- a/src/app/common/directives/node-versions.directive.ts +++ b/src/app/common/directives/node-versions.directive.ts @@ -23,13 +23,16 @@ * along with Alfresco. If not, see . */ -import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { Directive, HostListener, Input } from '@angular/core'; -import { TranslationService, NotificationService, AlfrescoApiService } from '@alfresco/adf-core'; +import { AlfrescoApiService } from '@alfresco/adf-core'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; import { VersionManagerDialogAdapterComponent } from '../../components/versions-dialog/version-manager-dialog-adapter.component'; import { MatDialog } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { SnackbarErrorAction } from '../../store/actions'; @Directive({ selector: '[acaNodeVersions]' @@ -40,19 +43,15 @@ export class NodeVersionsDirective { @Input('acaNodeVersions') node: MinimalNodeEntity; - @Output() - nodeVersionError: EventEmitter = new EventEmitter(); - @HostListener('click') onClick() { this.onManageVersions(); } constructor( + private store: Store, private apiService: AlfrescoApiService, - private dialog: MatDialog, - private notification: NotificationService, - private translation: TranslationService + private dialog: MatDialog ) {} async onManageVersions() { @@ -79,10 +78,7 @@ export class NodeVersionsDirective { VersionManagerDialogAdapterComponent, { data: { contentEntry }, panelClass: 'adf-version-manager-dialog', width: '630px' }); } else { - const translatedErrorMessage = this.translation.instant('APP.MESSAGES.ERRORS.PERMISSION'); - this.notification.openSnackMessage(translatedErrorMessage, 4000); - - this.nodeVersionError.emit(); + this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')); } } } diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index 48e71eb93..e1c47d5f7 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -25,83 +25,13 @@ import { Subject } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; -import { AlfrescoApiService, NotificationService, TranslationService } from '@alfresco/adf-core'; -import { Node } from 'alfresco-js-api'; - @Injectable() export class ContentManagementService { - - nodeDeleted = new Subject(); - nodeMoved = new Subject(); - nodeRestored = new Subject(); - - constructor(private api: AlfrescoApiService, - private notification: NotificationService, - private translation: TranslationService) { - } - - nodeHasPermission(node: Node, permission: string): boolean { - if (node && permission) { - const allowableOperations = node.allowableOperations || []; - - if (allowableOperations.indexOf(permission) > -1) { - return true; - } - } - - return false; - } - - canDeleteNode(node: Node): boolean { - return this.nodeHasPermission(node, 'delete'); - } - - canMoveNode(node: Node): boolean { - return this.nodeHasPermission(node, 'delete'); - } - - canCopyNode(node: Node): boolean { - return true; - } - - async deleteNode(node: Node) { - if (this.canDeleteNode(node)) { - try { - await this.api.nodesApi.deleteNode(node.id); - - this.notification - .openSnackMessageAction( - this.translation.instant('APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', { name: node.name }), - this.translation.translate.instant('APP.ACTIONS.UNDO'), - 10000 - ) - .onAction() - .subscribe(() => { - this.restoreNode(node); - }); - - this.nodeDeleted.next(node.id); - } catch { - this.notification.openSnackMessage( - this.translation.instant('APP.MESSAGES.ERRORS.NODE_DELETION', { name: node.name }), - 10000 - ); - } - } - } - - async restoreNode(node: Node) { - if (node) { - try { - await this.api.nodesApi.restoreNode(node.id); - this.nodeRestored.next(node.id); - } catch { - this.notification.openSnackMessage( - this.translation.instant('APP.MESSAGES.ERRORS.NODE_RESTORE', { name: node.name }), - 3000 - ); - } - } - } + nodesMoved = new Subject(); + nodesDeleted = new Subject(); + nodesPurged = new Subject(); + nodesRestored = new Subject(); + folderEdited = new Subject(); + folderCreated = new Subject(); } diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 6a50e9448..891235405 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -26,9 +26,8 @@ mat-icon-button color="primary" *ngIf="selectedFolder" - [attr.title]="'APP.ACTIONS.EDIT' | translate" - (error)="openSnackMessage($event)" - [adf-edit-folder]="selectedFolder?.entry"> + title="{{ 'APP.ACTIONS.EDIT' | translate }}" + [acaEditFolder]="selectedFolder"> create @@ -98,7 +97,7 @@ [navigate]="false" [sorting]="[ 'modifiedAt', 'desc' ]" [acaSortingPreferenceKey]="sortingPreferenceKey" - (node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)" + (node-dblclick)="onNodeDoubleClick($event.detail?.node)" (ready)="onDocumentListReady($event, documentList)" (node-select)="onNodeSelect($event, documentList)" (node-unselect)="onNodeUnselect($event, documentList)"> diff --git a/src/app/components/favorites/favorites.component.spec.ts b/src/app/components/favorites/favorites.component.spec.ts index 7b5e7bf88..806666799 100644 --- a/src/app/components/favorites/favorites.component.spec.ts +++ b/src/app/components/favorites/favorites.component.spec.ts @@ -51,14 +51,12 @@ import { StoreModule } from '@ngrx/store'; import { appReducer } from '../../store/reducers/app.reducer'; import { INITIAL_STATE } from '../../store/states/app.state'; -describe('Favorites Routed Component', () => { +describe('FavoritesComponent', () => { let fixture: ComponentFixture; let component: FavoritesComponent; let nodesApi: NodesApiService; let alfrescoApi: AlfrescoApiService; - let alfrescoContentService: ContentService; let contentService: ContentManagementService; - let notificationService: NotificationService; let router: Router; let page; let node; @@ -132,10 +130,8 @@ describe('Favorites Routed Component', () => { component = fixture.componentInstance; nodesApi = TestBed.get(NodesApiService); - notificationService = TestBed.get(NotificationService); alfrescoApi = TestBed.get(AlfrescoApiService); alfrescoApi.reset(); - alfrescoContentService = TestBed.get(ContentService); contentService = TestBed.get(ContentManagementService); router = TestBed.get(Router); }); @@ -152,25 +148,25 @@ describe('Favorites Routed Component', () => { }); it('should refresh on editing folder event', () => { - alfrescoContentService.folderEdit.next(null); + contentService.folderEdited.next(null); expect(component.reload).toHaveBeenCalled(); }); it('should refresh on move node event', () => { - contentService.nodeMoved.next(null); + contentService.nodesMoved.next(null); expect(component.reload).toHaveBeenCalled(); }); it('should refresh on node deleted event', () => { - contentService.nodeDeleted.next(null); + contentService.nodesDeleted.next(null); expect(component.reload).toHaveBeenCalled(); }); it('should refresh on node restore event', () => { - contentService.nodeRestored.next(null); + contentService.nodesRestored.next(null); expect(component.reload).toHaveBeenCalled(); }); @@ -218,7 +214,7 @@ describe('Favorites Routed Component', () => { node.isFolder = true; spyOn(router, 'navigate'); - component.onNodeDoubleClick(node); + component.onNodeDoubleClick({ entry: node }); expect(router.navigate).toHaveBeenCalled(); }); @@ -228,7 +224,7 @@ describe('Favorites Routed Component', () => { node.isFile = true; spyOn(router, 'navigate').and.stub(); - component.onNodeDoubleClick(node); + component.onNodeDoubleClick({ entry: node }); expect(router.navigate['calls'].argsFor(0)[0]).toEqual(['./preview', 'folder-node']); }); @@ -244,16 +240,4 @@ describe('Favorites Routed Component', () => { expect(component.documentList.reload).toHaveBeenCalled(); }); }); - - describe('openSnackMessage', () => { - it('should call notification service', () => { - const message = 'notification message'; - - spyOn(notificationService, 'openSnackMessage'); - - component.openSnackMessage(message); - - expect(notificationService.openSnackMessage).toHaveBeenCalledWith(message, 4000); - }); - }); }); diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts index e2083f774..5754f7f65 100644 --- a/src/app/components/favorites/favorites.component.ts +++ b/src/app/components/favorites/favorites.component.ts @@ -25,8 +25,8 @@ import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; -import { MinimalNodeEntryEntity, PathElementEntity, PathInfo } from 'alfresco-js-api'; -import { ContentService, NodesApiService, UserPreferencesService, NotificationService } from '@alfresco/adf-core'; +import { MinimalNodeEntryEntity, PathElementEntity, PathInfo, MinimalNodeEntity } from 'alfresco-js-api'; +import { NodesApiService, UserPreferencesService } from '@alfresco/adf-core'; import { ContentManagementService } from '../../common/services/content-management.service'; import { NodePermissionService } from '../../common/services/node-permission.service'; @@ -43,9 +43,7 @@ export class FavoritesComponent extends PageComponent implements OnInit { route: ActivatedRoute, store: Store, private nodesApi: NodesApiService, - private contentService: ContentService, private content: ContentManagementService, - private notificationService: NotificationService, public permission: NodePermissionService, preferences: UserPreferencesService) { super(preferences, router, route, store); @@ -55,10 +53,10 @@ export class FavoritesComponent extends PageComponent implements OnInit { super.ngOnInit(); this.subscriptions = this.subscriptions.concat([ - this.content.nodeDeleted.subscribe(() => this.reload()), - this.content.nodeRestored.subscribe(() => this.reload()), - this.contentService.folderEdit.subscribe(() => this.reload()), - this.content.nodeMoved.subscribe(() => this.reload()) + this.content.nodesDeleted.subscribe(() => this.reload()), + this.content.nodesRestored.subscribe(() => this.reload()), + this.content.folderEdited.subscribe(() => this.reload()), + this.content.nodesMoved.subscribe(() => this.reload()) ]); } @@ -80,22 +78,13 @@ export class FavoritesComponent extends PageComponent implements OnInit { } } - onNodeDoubleClick(node: MinimalNodeEntryEntity) { - if (node) { - if (node.isFolder) { - this.navigate(node); + onNodeDoubleClick(node: MinimalNodeEntity) { + if (node && node.entry) { + if (node.entry.isFolder) { + this.navigate(node.entry); } - if (node.isFile) { - this.router.navigate(['./preview', node.id], { relativeTo: this.route }); - } + this.showPreview(node); } } - - openSnackMessage(event: any) { - this.notificationService.openSnackMessage( - event, - 4000 - ); - } } diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index e4a41e09b..126c867ff 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -28,9 +28,8 @@ color="primary" mat-icon-button *ngIf="selectedFolder && permission.check(selectedFolder, ['update'])" - [attr.title]="'APP.ACTIONS.EDIT' | translate" - (error)="openSnackMessage($event)" - [adf-edit-folder]="selectedFolder?.entry"> + title="{{ 'APP.ACTIONS.EDIT' | translate }}" + [acaEditFolder]="selectedFolder"> create diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts index bf9642628..9b05e2b0b 100644 --- a/src/app/components/files/files.component.spec.ts +++ b/src/app/components/files/files.component.spec.ts @@ -58,13 +58,11 @@ describe('FilesComponent', () => { let fixture; let component: FilesComponent; let contentManagementService: ContentManagementService; - let alfrescoContentService: ContentService; let uploadService: UploadService; let nodesApi: NodesApiService; let router: Router; let browsingFilesService: BrowsingFilesService; let nodeActionsService: NodeActionsService; - let notificationService: NotificationService; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -122,9 +120,7 @@ describe('FilesComponent', () => { uploadService = TestBed.get(UploadService); nodesApi = TestBed.get(NodesApiService); router = TestBed.get(Router); - alfrescoContentService = TestBed.get(ContentService); browsingFilesService = TestBed.get(BrowsingFilesService); - notificationService = TestBed.get(NotificationService); nodeActionsService = TestBed.get(NodeActionsService); }); })); @@ -243,31 +239,31 @@ describe('FilesComponent', () => { }); it('should call refresh onCreateFolder event', () => { - alfrescoContentService.folderCreate.next(); + contentManagementService.folderCreated.next(); expect(component.documentList.reload).toHaveBeenCalled(); }); it('should call refresh editFolder event', () => { - alfrescoContentService.folderEdit.next(); + contentManagementService.folderEdited.next(); expect(component.documentList.reload).toHaveBeenCalled(); }); it('should call refresh deleteNode event', () => { - contentManagementService.nodeDeleted.next(); + contentManagementService.nodesDeleted.next(); expect(component.documentList.reload).toHaveBeenCalled(); }); it('should call refresh moveNode event', () => { - contentManagementService.nodeMoved.next(); + contentManagementService.nodesMoved.next(); expect(component.documentList.reload).toHaveBeenCalled(); }); it('should call refresh restoreNode event', () => { - contentManagementService.nodeRestored.next(); + contentManagementService.nodesRestored.next(); expect(component.documentList.reload).toHaveBeenCalled(); }); @@ -453,16 +449,4 @@ describe('FilesComponent', () => { expect(component.isSiteContainer(mock)).toBe(true); }); }); - - describe('openSnackMessage', () => { - it('should call notification service', () => { - const message = 'notification message'; - - spyOn(notificationService, 'openSnackMessage'); - - component.openSnackMessage(message); - - expect(notificationService.openSnackMessage).toHaveBeenCalledWith(message, 4000); - }); - }); }); diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index b1cd8e1ec..0e1b4e583 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -29,7 +29,7 @@ import { Router, ActivatedRoute, Params } from '@angular/router'; import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElementEntity, NodePaging, PathElement } from 'alfresco-js-api'; import { UploadService, FileUploadEvent, NodesApiService, - ContentService, AlfrescoApiService, UserPreferencesService, NotificationService + AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core'; import { BrowsingFilesService } from '../../common/services/browsing-files.service'; @@ -58,9 +58,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { private uploadService: UploadService, private contentManagementService: ContentManagementService, private browsingFilesService: BrowsingFilesService, - private contentService: ContentService, private apiService: AlfrescoApiService, - private notificationService: NotificationService, public permission: NodePermissionService, preferences: UserPreferencesService) { super(preferences, router, route, store); @@ -69,7 +67,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { ngOnInit() { super.ngOnInit(); - const { route, contentManagementService, contentService, nodeActionsService, uploadService } = this; + const { route, contentManagementService, nodeActionsService, uploadService } = this; const { data } = route.snapshot; this.title = data.title; @@ -99,11 +97,11 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { this.subscriptions = this.subscriptions.concat([ nodeActionsService.contentCopied.subscribe((nodes) => this.onContentCopied(nodes)), - contentService.folderCreate.subscribe(() => this.documentList.reload()), - contentService.folderEdit.subscribe(() => this.documentList.reload()), - contentManagementService.nodeDeleted.subscribe(() => this.documentList.reload()), - contentManagementService.nodeMoved.subscribe(() => this.documentList.reload()), - contentManagementService.nodeRestored.subscribe(() => this.documentList.reload()), + contentManagementService.folderCreated.subscribe(() => this.documentList.reload()), + contentManagementService.folderEdited.subscribe(() => this.documentList.reload()), + contentManagementService.nodesDeleted.subscribe(() => this.documentList.reload()), + contentManagementService.nodesMoved.subscribe(() => this.documentList.reload()), + contentManagementService.nodesRestored.subscribe(() => this.documentList.reload()), uploadService.fileUploadComplete.subscribe(file => this.onFileUploadedEvent(file)), uploadService.fileUploadDeleted.subscribe((file) => this.onFileUploadedEvent(file)) ]); @@ -253,11 +251,4 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { } return false; } - - openSnackMessage(event: any) { - this.notificationService.openSnackMessage( - event, - 4000 - ); - } } diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 78a9afbbf..61efe7c6b 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -31,7 +31,7 @@ import { OnDestroy, ViewChild, OnInit } from '@angular/core'; import { Subscription, Subject } from 'rxjs/Rx'; import { Store } from '@ngrx/store'; import { AppStore } from '../store/states/app.state'; -import { SetSelectedNodesAction } from '../store/actions/select-nodes.action'; +import { SetSelectedNodesAction } from '../store/actions/node.action'; import { selectedNodes } from '../store/selectors/app.selectors'; import { takeUntil } from 'rxjs/operators'; @@ -101,10 +101,8 @@ export abstract class PageComponent implements OnInit, OnDestroy { } showPreview(node: MinimalNodeEntity) { - if (node && node.entry) { - if (node.entry.isFile) { - this.router.navigate(['./preview', node.entry.id], { relativeTo: this.route }); - } + if (node && node.entry && node.entry.isFile) { + this.router.navigate(['./preview', node.entry.id], { relativeTo: this.route }); } } diff --git a/src/app/components/preview/preview.component.spec.ts b/src/app/components/preview/preview.component.spec.ts index f8c7e150e..1bc2bd754 100644 --- a/src/app/components/preview/preview.component.spec.ts +++ b/src/app/components/preview/preview.component.spec.ts @@ -28,17 +28,20 @@ import { Router, ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { - AlfrescoApiService, UserPreferencesService, TranslationService, TranslationMock, - AppConfigService, StorageService, CookieService, NotificationService, NodeFavoriteDirective + AlfrescoApiService, UserPreferencesService, + TranslationService, TranslationMock, + CoreModule } from '@alfresco/adf-core'; -import { TranslateModule } from '@ngx-translate/core'; -import { HttpClientModule } from '@angular/common/http'; import { PreviewComponent } from './preview.component'; import { Observable } from 'rxjs/Rx'; import { NodePermissionService } from '../../common/services/node-permission.service'; import { ContentManagementService } from '../../common/services/content-management.service'; -import { MatSnackBarModule } from '@angular/material'; +import { StoreModule } from '@ngrx/store'; +import { appReducer } from '../../store/reducers/app.reducer'; +import { INITIAL_STATE } from '../../store/states/app.state'; +import { EffectsModule } from '@ngrx/effects'; +import { NodeEffects } from '../../store/effects/node.effects'; describe('PreviewComponent', () => { @@ -52,25 +55,19 @@ describe('PreviewComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - HttpClientModule, RouterTestingModule, - TranslateModule.forRoot(), - MatSnackBarModule + CoreModule.forRoot(), + StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), + EffectsModule.forRoot([NodeEffects]) ], providers: [ { provide: TranslationService, useClass: TranslationMock }, - AlfrescoApiService, - AppConfigService, - StorageService, - CookieService, - NotificationService, - UserPreferencesService, NodePermissionService, ContentManagementService ], declarations: [ PreviewComponent, - NodeFavoriteDirective + // NodeFavoriteDirective ], schemas: [ NO_ERRORS_SCHEMA ] }) diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index 1a84f6b50..554a22f2c 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -28,7 +28,9 @@ import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_O import { AlfrescoApiService, UserPreferencesService, ObjectUtils } from '@alfresco/adf-core'; import { Node, MinimalNodeEntity } from 'alfresco-js-api'; import { NodePermissionService } from '../../common/services/node-permission.service'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { DeleteNodesAction } from '../../store/actions'; @Component({ selector: 'app-preview', @@ -54,11 +56,12 @@ export class PreviewComponent implements OnInit { selectedEntities: MinimalNodeEntity[] = []; - constructor(private router: Router, + constructor( + private store: Store, + private router: Router, private route: ActivatedRoute, private apiService: AlfrescoApiService, private preferences: UserPreferencesService, - private content: ContentManagementService, public permission: NodePermissionService) { } @@ -326,12 +329,14 @@ export class PreviewComponent implements OnInit { return path; } - async deleteFile() { - try { - await this.content.deleteNode(this.node); - this.onVisibilityChanged(false); - } catch { - } + deleteFile() { + this.store.dispatch(new DeleteNodesAction([ + { + id: this.node.nodeId || this.node.id, + name: this.node.name + } + ])); + this.onVisibilityChanged(false); } private getNavigationCommands(url: string): any[] { diff --git a/src/app/components/recent-files/recent-files.component.spec.ts b/src/app/components/recent-files/recent-files.component.spec.ts index c89aa944b..c74b89c87 100644 --- a/src/app/components/recent-files/recent-files.component.spec.ts +++ b/src/app/components/recent-files/recent-files.component.spec.ts @@ -130,7 +130,7 @@ describe('RecentFiles Routed Component', () => { it('should reload nodes on onDeleteNode event', () => { fixture.detectChanges(); - contentService.nodeDeleted.next(); + contentService.nodesDeleted.next(); expect(component.reload).toHaveBeenCalled(); }); @@ -138,7 +138,7 @@ describe('RecentFiles Routed Component', () => { it('should reload on onRestoreNode event', () => { fixture.detectChanges(); - contentService.nodeRestored.next(); + contentService.nodesRestored.next(); expect(component.reload).toHaveBeenCalled(); }); @@ -146,7 +146,7 @@ describe('RecentFiles Routed Component', () => { it('should reload on move node event', () => { fixture.detectChanges(); - contentService.nodeMoved.next(); + contentService.nodesMoved.next(); expect(component.reload).toHaveBeenCalled(); }); diff --git a/src/app/components/recent-files/recent-files.component.ts b/src/app/components/recent-files/recent-files.component.ts index 32a5a1038..0e2670e89 100644 --- a/src/app/components/recent-files/recent-files.component.ts +++ b/src/app/components/recent-files/recent-files.component.ts @@ -53,9 +53,9 @@ export class RecentFilesComponent extends PageComponent implements OnInit { super.ngOnInit(); this.subscriptions = this.subscriptions.concat([ - this.content.nodeDeleted.subscribe(() => this.reload()), - this.content.nodeMoved.subscribe(() => this.reload()), - this.content.nodeRestored.subscribe(() => this.reload()) + this.content.nodesDeleted.subscribe(() => this.reload()), + this.content.nodesMoved.subscribe(() => this.reload()), + this.content.nodesRestored.subscribe(() => this.reload()) ]); } diff --git a/src/app/components/shared-files/shared-files.component.spec.ts b/src/app/components/shared-files/shared-files.component.spec.ts index ab1c3a15a..1ebf9755d 100644 --- a/src/app/components/shared-files/shared-files.component.spec.ts +++ b/src/app/components/shared-files/shared-files.component.spec.ts @@ -131,7 +131,7 @@ describe('SharedFilesComponent', () => { it('should refresh on deleteNode event', () => { fixture.detectChanges(); - contentService.nodeDeleted.next(); + contentService.nodesDeleted.next(); expect(component.reload).toHaveBeenCalled(); }); @@ -139,7 +139,7 @@ describe('SharedFilesComponent', () => { it('should refresh on restoreNode event', () => { fixture.detectChanges(); - contentService.nodeRestored.next(); + contentService.nodesRestored.next(); expect(component.reload).toHaveBeenCalled(); }); @@ -147,7 +147,7 @@ describe('SharedFilesComponent', () => { it('should reload on move node event', () => { fixture.detectChanges(); - contentService.nodeMoved.next(); + contentService.nodesMoved.next(); expect(component.reload).toHaveBeenCalled(); }); @@ -170,17 +170,6 @@ describe('SharedFilesComponent', () => { expect(router.navigate['calls'].argsFor(0)[0]).toEqual(['./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 } })); diff --git a/src/app/components/shared-files/shared-files.component.ts b/src/app/components/shared-files/shared-files.component.ts index eb3ea2074..47f9af629 100644 --- a/src/app/components/shared-files/shared-files.component.ts +++ b/src/app/components/shared-files/shared-files.component.ts @@ -25,8 +25,7 @@ import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; -import { MinimalNodeEntity } from 'alfresco-js-api'; -import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core'; +import { UserPreferencesService } from '@alfresco/adf-core'; import { ContentManagementService } from '../../common/services/content-management.service'; import { NodePermissionService } from '../../common/services/node-permission.service'; @@ -43,7 +42,6 @@ export class SharedFilesComponent extends PageComponent implements OnInit { route: ActivatedRoute, store: Store, private content: ContentManagementService, - private apiService: AlfrescoApiService, public permission: NodePermissionService, preferences: UserPreferencesService) { super(preferences, router, route, store); @@ -53,21 +51,15 @@ export class SharedFilesComponent extends PageComponent implements OnInit { super.ngOnInit(); this.subscriptions = this.subscriptions.concat([ - this.content.nodeDeleted.subscribe(() => this.reload()), - this.content.nodeMoved.subscribe(() => this.reload()), - this.content.nodeRestored.subscribe(() => this.reload()) + this.content.nodesDeleted.subscribe(() => this.reload()), + this.content.nodesMoved.subscribe(() => this.reload()), + this.content.nodesRestored.subscribe(() => this.reload()) ]); } 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], { relativeTo: this.route }); - } - } - ); + this.router.navigate(['./preview', link.nodeId], { relativeTo: this.route }); } } } diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 87708b9db..e42cb40be 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -9,8 +9,7 @@ @@ -16,7 +15,6 @@