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 @@
{
let fixture;
let component: SidenavComponent;
let browsingService: BrowsingFilesService;
let appConfig: AppConfigService;
- let notificationService: NotificationService;
let appConfigSpy;
const navItem = {
@@ -60,28 +59,22 @@ describe('SidenavComponent', () => {
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
- HttpClientModule,
+ CoreModule.forRoot(),
MatMenuModule,
MatSnackBarModule,
- TranslateModule.forRoot(),
RouterTestingModule,
- ElectronModule
+ ElectronModule,
+ StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
+ EffectsModule.forRoot([NodeEffects])
],
declarations: [
SidenavComponent
],
providers: [
{ provide: TranslationService, useClass: TranslationMock },
- LogService,
- CookieService,
- AlfrescoApiService,
- StorageService,
- UserPreferencesService,
- AuthenticationService,
NodePermissionService,
- AppConfigService,
BrowsingFilesService,
- NotificationService
+ ContentManagementService
],
schemas: [ NO_ERRORS_SCHEMA ]
})
@@ -89,7 +82,6 @@ describe('SidenavComponent', () => {
.then(() => {
browsingService = TestBed.get(BrowsingFilesService);
appConfig = TestBed.get(AppConfigService);
- notificationService = TestBed.get(NotificationService);
fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance;
@@ -122,16 +114,4 @@ describe('SidenavComponent', () => {
expect(component.navigation).toEqual([[navItem, navItem], [navItem, navItem]]);
});
});
-
- 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/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts
index 385c96df6..0df82e90a 100644
--- a/src/app/components/sidenav/sidenav.component.ts
+++ b/src/app/components/sidenav/sidenav.component.ts
@@ -26,7 +26,7 @@
import { Subscription } from 'rxjs/Rx';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
-import { AppConfigService, NotificationService } from '@alfresco/adf-core';
+import { AppConfigService } from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
@@ -48,7 +48,6 @@ export class SidenavComponent implements OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
constructor(
- private notificationService: NotificationService,
private browsingFilesService: BrowsingFilesService,
private appConfig: AppConfigService,
public permission: NodePermissionService,
@@ -66,13 +65,6 @@ export class SidenavComponent implements OnInit, OnDestroy {
this.isDesktopApp = this.electronService.isDesktopApp;
}
- openSnackMessage(event: any) {
- this.notificationService.openSnackMessage(
- event,
- 4000
- );
- }
-
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html
index 033fa19d4..3e0c7d956 100644
--- a/src/app/components/trashcan/trashcan.component.html
+++ b/src/app/components/trashcan/trashcan.component.html
@@ -7,8 +7,7 @@
delete_forever
@@ -16,7 +15,6 @@
restore
diff --git a/src/app/components/trashcan/trashcan.component.spec.ts b/src/app/components/trashcan/trashcan.component.spec.ts
index d646e10e1..2dbc0f1ec 100644
--- a/src/app/components/trashcan/trashcan.component.spec.ts
+++ b/src/app/components/trashcan/trashcan.component.spec.ts
@@ -124,7 +124,7 @@ describe('TrashcanComponent', () => {
spyOn(component, 'reload');
fixture.detectChanges();
- contentService.nodeRestored.next();
+ contentService.nodesRestored.next();
expect(component.reload).toHaveBeenCalled();
});
diff --git a/src/app/components/trashcan/trashcan.component.ts b/src/app/components/trashcan/trashcan.component.ts
index efea722aa..5fa4648f6 100644
--- a/src/app/components/trashcan/trashcan.component.ts
+++ b/src/app/components/trashcan/trashcan.component.ts
@@ -49,7 +49,9 @@ export class TrashcanComponent extends PageComponent implements OnInit {
super.ngOnInit();
this.subscriptions.push(
- this.contentManagementService.nodeRestored.subscribe(() => this.reload())
+ this.contentManagementService.nodesRestored.subscribe(() => this.reload()),
+ this.contentManagementService.nodesPurged.subscribe(() => this.reload()),
+ this.contentManagementService.nodesRestored.subscribe(() => this.reload())
);
}
diff --git a/src/app/directives/create-folder.directive.ts b/src/app/directives/create-folder.directive.ts
new file mode 100644
index 000000000..4dcb19015
--- /dev/null
+++ b/src/app/directives/create-folder.directive.ts
@@ -0,0 +1,89 @@
+/*!
+ * @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 .
+ */
+
+import { Directive, HostListener, Input } from '@angular/core';
+import { MatDialog, MatDialogConfig } from '@angular/material';
+import { FolderDialogComponent } from '@alfresco/adf-content-services';
+import { Store } from '@ngrx/store';
+import { AppStore } from '../store/states/app.state';
+import { SnackbarErrorAction } from '../store/actions';
+import { ContentManagementService } from '../common/services/content-management.service';
+
+@Directive({
+ selector: '[acaCreateFolder]'
+})
+export class CreateFolderDirective {
+ /** Parent folder where the new folder will be located after creation. */
+ // tslint:disable-next-line:no-input-rename
+ @Input('acaCreateFolder') parentNodeId: string;
+
+ /** Title of folder creation dialog. */
+ @Input() dialogTitle: string = null;
+
+ /** Type of node to create. */
+ @Input() nodeType = 'cm:folder';
+
+ @HostListener('click', ['$event'])
+ onClick(event: Event) {
+ if (this.parentNodeId) {
+ event.preventDefault();
+ this.openDialog();
+ }
+ }
+
+ constructor(
+ private store: Store,
+ private dialogRef: MatDialog,
+ private content: ContentManagementService
+ ) {}
+
+ private get dialogConfig(): MatDialogConfig {
+ return {
+ data: {
+ parentNodeId: this.parentNodeId,
+ createTitle: this.dialogTitle,
+ nodeType: this.nodeType
+ },
+ width: '400px'
+ };
+ }
+
+ private openDialog(): void {
+ const dialogInstance = this.dialogRef.open(
+ FolderDialogComponent,
+ this.dialogConfig
+ );
+
+ dialogInstance.componentInstance.error.subscribe(message => {
+ this.store.dispatch(new SnackbarErrorAction(message));
+ });
+
+ dialogInstance.afterClosed().subscribe(node => {
+ if (node) {
+ this.content.folderCreated.next(node);
+ }
+ });
+ }
+}
diff --git a/src/app/directives/edit-folder.directive.ts b/src/app/directives/edit-folder.directive.ts
new file mode 100644
index 000000000..e532ec87d
--- /dev/null
+++ b/src/app/directives/edit-folder.directive.ts
@@ -0,0 +1,82 @@
+/*!
+ * @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 .
+ */
+
+import { Directive, Input, HostListener } from '@angular/core';
+import { MinimalNodeEntryEntity, MinimalNodeEntity } from 'alfresco-js-api';
+import { MatDialog, MatDialogConfig } from '@angular/material';
+import { FolderDialogComponent } from '@alfresco/adf-content-services';
+import { Store } from '@ngrx/store';
+import { AppStore } from '../store/states/app.state';
+import { SnackbarErrorAction } from '../store/actions';
+import { ContentManagementService } from '../common/services/content-management.service';
+
+@Directive({
+ selector: '[acaEditFolder]'
+})
+export class EditFolderDirective {
+
+ /** Folder node to edit. */
+ // tslint:disable-next-line:no-input-rename
+ @Input('acaEditFolder')
+ folder: MinimalNodeEntity;
+
+ @HostListener('click', [ '$event' ])
+ onClick(event) {
+ event.preventDefault();
+
+ if (this.folder) {
+ this.openDialog();
+ }
+ }
+
+ constructor(
+ private store: Store,
+ private dialogRef: MatDialog,
+ private content: ContentManagementService
+ ) {}
+
+ private get dialogConfig(): MatDialogConfig {
+ return {
+ data: {
+ folder: this.folder.entry
+ },
+ width: '400px'
+ };
+ }
+
+ private openDialog(): void {
+ const dialog = this.dialogRef.open(FolderDialogComponent, this.dialogConfig);
+
+ dialog.componentInstance.error.subscribe(message => {
+ this.store.dispatch(new SnackbarErrorAction(message));
+ });
+
+ dialog.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
+ if (node) {
+ this.content.folderEdited.next(node);
+ }
+ });
+ }
+}
diff --git a/src/app/store/actions.ts b/src/app/store/actions.ts
new file mode 100644
index 000000000..cbc694c2a
--- /dev/null
+++ b/src/app/store/actions.ts
@@ -0,0 +1,6 @@
+export * from './actions/app-name.action';
+export * from './actions/header-color.action';
+export * from './actions/logo-path.action';
+export * from './actions/node.action';
+export * from './actions/snackbar.action';
+export * from './actions/router.action';
diff --git a/src/app/store/actions/node.action.ts b/src/app/store/actions/node.action.ts
new file mode 100644
index 000000000..e76bd7bc5
--- /dev/null
+++ b/src/app/store/actions/node.action.ts
@@ -0,0 +1,37 @@
+import { Action } from '@ngrx/store';
+
+export const SET_SELECTED_NODES = 'SET_SELECTED_NODES';
+export const DELETE_NODES = 'DELETE_NODES';
+export const UNDO_DELETE_NODES = 'UNDO_DELETE_NODES';
+export const RESTORE_DELETED_NODES = 'RESTORE_DELETED_NODES';
+export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES';
+
+export interface NodeInfo {
+ id: string;
+ name: string;
+}
+
+export class SetSelectedNodesAction implements Action {
+ readonly type = SET_SELECTED_NODES;
+ constructor(public payload: any[] = []) {}
+}
+
+export class DeleteNodesAction implements Action {
+ readonly type = DELETE_NODES;
+ constructor(public payload: NodeInfo[] = []) {}
+}
+
+export class UndoDeleteNodesAction implements Action {
+ readonly type = UNDO_DELETE_NODES;
+ constructor(public payload: any[] = []) {}
+}
+
+export class RestoreDeletedNodesAction implements Action {
+ readonly type = RESTORE_DELETED_NODES;
+ constructor(public payload: any[] = []) {}
+}
+
+export class PurgeDeletedNodesAction implements Action {
+ readonly type = PURGE_DELETED_NODES;
+ constructor(public payload: NodeInfo[] = []) {}
+}
diff --git a/src/app/store/actions/router.action.ts b/src/app/store/actions/router.action.ts
new file mode 100644
index 000000000..859f4918c
--- /dev/null
+++ b/src/app/store/actions/router.action.ts
@@ -0,0 +1,8 @@
+import { Action } from '@ngrx/store';
+
+export const NAVIGATE_ROUTE = 'NAVIGATE_ROUTE';
+
+export class NavigateRouteAction implements Action {
+ readonly type = NAVIGATE_ROUTE;
+ constructor(public payload: any[]) {}
+}
diff --git a/src/app/store/actions/select-nodes.action.ts b/src/app/store/actions/select-nodes.action.ts
deleted file mode 100644
index 1e0646fd5..000000000
--- a/src/app/store/actions/select-nodes.action.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Action } from '@ngrx/store';
-
-export const SET_SELECTED_NODES = 'SET_SELECTED_NODES';
-
-export class SetSelectedNodesAction implements Action {
- readonly type = SET_SELECTED_NODES;
- constructor(public payload: any[] = []) {}
-}
diff --git a/src/app/store/actions/snackbar.action.ts b/src/app/store/actions/snackbar.action.ts
new file mode 100644
index 000000000..564addeed
--- /dev/null
+++ b/src/app/store/actions/snackbar.action.ts
@@ -0,0 +1,43 @@
+import { Action } from '@ngrx/store';
+
+export const SNACKBAR_INFO = 'SNACKBAR_INFO';
+export const SNACKBAR_WARNING = 'SNACKBAR_WARNING';
+export const SNACKBAR_ERROR = 'SNACKBAR_ERROR';
+
+export interface SnackbarAction extends Action {
+ payload: string;
+ params?: Object;
+ userAction?: SnackbarUserAction;
+ duration: number;
+}
+
+export class SnackbarUserAction {
+ constructor(public title: string, public action: Action) {}
+}
+
+export class SnackbarInfoAction implements SnackbarAction {
+ readonly type = SNACKBAR_INFO;
+
+ userAction?: SnackbarUserAction;
+ duration = 4000;
+
+ constructor(public payload: string, public params?: Object) {}
+}
+
+export class SnackbarWarningAction implements SnackbarAction {
+ readonly type = SNACKBAR_WARNING;
+
+ userAction?: SnackbarUserAction;
+ duration = 4000;
+
+ constructor(public payload: string, public params?: Object) {}
+}
+
+export class SnackbarErrorAction implements SnackbarAction {
+ readonly type = SNACKBAR_ERROR;
+
+ userAction?: SnackbarUserAction;
+ duration = 4000;
+
+ constructor(public payload: string, public params?: Object) {}
+}
diff --git a/src/app/store/effects/node.effects.ts b/src/app/store/effects/node.effects.ts
new file mode 100644
index 000000000..ce52b664b
--- /dev/null
+++ b/src/app/store/effects/node.effects.ts
@@ -0,0 +1,353 @@
+import { Effect, Actions, ofType } from '@ngrx/effects';
+import { Injectable } from '@angular/core';
+import { map } from 'rxjs/operators';
+import { DeleteStatus } from '../../common/directives/delete-status.interface';
+import { Store } from '@ngrx/store';
+import { AppStore } from '../states/app.state';
+import {
+ SnackbarWarningAction,
+ SnackbarInfoAction,
+ SnackbarErrorAction,
+ PurgeDeletedNodesAction,
+ PURGE_DELETED_NODES,
+ NodeInfo,
+ DeleteNodesAction,
+ DELETE_NODES,
+ SnackbarUserAction,
+ SnackbarAction,
+ UndoDeleteNodesAction,
+ UNDO_DELETE_NODES
+} from '../actions';
+import { ContentManagementService } from '../../common/services/content-management.service';
+import { Observable } from 'rxjs/Rx';
+import { DeletedNodeInfo } from '../../common/directives/deleted-node-info.interface';
+import { AlfrescoApiService } from '@alfresco/adf-core';
+
+@Injectable()
+export class NodeEffects {
+ constructor(
+ private store: Store,
+ private actions$: Actions,
+ private contentManagementService: ContentManagementService,
+ private alfrescoApiService: AlfrescoApiService
+ ) {}
+
+ @Effect({ dispatch: false })
+ purgeDeletedNodes$ = this.actions$.pipe(
+ ofType(PURGE_DELETED_NODES),
+ map(action => {
+ this.purgeNodes(action.payload);
+ })
+ );
+
+ @Effect({ dispatch: false })
+ deleteNodes$ = this.actions$.pipe(
+ ofType(DELETE_NODES),
+ map(action => {
+ if (action.payload.length > 0) {
+ this.deleteNodes(action.payload);
+ }
+ })
+ );
+
+ @Effect({ dispatch: false })
+ undoDeleteNodes$ = this.actions$.pipe(
+ ofType(UNDO_DELETE_NODES),
+ map(action => {
+ if (action.payload.length > 0) {
+ this.undoDeleteNodes(action.payload);
+ }
+ })
+ );
+
+ private deleteNodes(items: NodeInfo[]): void {
+ const batch: Observable[] = [];
+
+ items.forEach(node => {
+ batch.push(this.deleteNode(node));
+ });
+
+ Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => {
+ const status = this.processStatus(data);
+ const message = this.getDeleteMessage(status);
+
+ if (message && status.someSucceeded) {
+ message.duration = 10000;
+ message.userAction = new SnackbarUserAction(
+ 'APP.ACTIONS.UNDO',
+ new UndoDeleteNodesAction([...status.success])
+ );
+ }
+
+ this.store.dispatch(message);
+
+ if (status.someSucceeded) {
+ this.contentManagementService.nodesDeleted.next();
+ }
+ });
+ }
+
+ private deleteNode(node: NodeInfo): Observable {
+ const { id, name } = node;
+
+ return Observable.fromPromise(
+ this.alfrescoApiService.nodesApi.deleteNode(id)
+ )
+ .map(() => {
+ return {
+ id,
+ name,
+ status: 1
+ };
+ })
+ .catch((error: any) => {
+ return Observable.of({
+ id,
+ name,
+ status: 0
+ });
+ });
+ }
+
+ private getDeleteMessage(status: DeleteStatus): SnackbarAction {
+ if (status.allFailed && !status.oneFailed) {
+ return new SnackbarErrorAction(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL',
+ { number: status.fail.length }
+ );
+ }
+
+ if (status.allSucceeded && !status.oneSucceeded) {
+ return new SnackbarInfoAction(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL',
+ { number: status.success.length }
+ );
+ }
+
+ if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
+ return new SnackbarWarningAction(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL',
+ {
+ success: status.success.length,
+ failed: status.fail.length
+ }
+ );
+ }
+
+ if (status.someFailed && status.oneSucceeded) {
+ return new SnackbarWarningAction(
+ 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR',
+ {
+ success: status.success.length,
+ failed: status.fail.length
+ }
+ );
+ }
+
+ if (status.oneFailed && !status.someSucceeded) {
+ return new SnackbarErrorAction(
+ 'APP.MESSAGES.ERRORS.NODE_DELETION',
+ { name: status.fail[0].name }
+ );
+ }
+
+ if (status.oneSucceeded && !status.someFailed) {
+ return new SnackbarInfoAction(
+ 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR',
+ { name: status.success[0].name }
+ );
+ }
+
+ return null;
+ }
+
+ private undoDeleteNodes(items: DeletedNodeInfo[]): void {
+ const batch: Observable[] = [];
+
+ items.forEach(item => {
+ batch.push(this.undoDeleteNode(item));
+ });
+
+ Observable.forkJoin(...batch).subscribe(data => {
+ const processedData = this.processStatus(data);
+
+ if (processedData.fail.length) {
+ const message = this.getUndoDeleteMessage(processedData);
+ this.store.dispatch(message);
+ }
+
+ if (processedData.someSucceeded) {
+ this.contentManagementService.nodesRestored.next();
+ }
+ });
+ }
+
+ private undoDeleteNode(item: DeletedNodeInfo): Observable {
+ const { id, name } = item;
+
+ return Observable.fromPromise(
+ this.alfrescoApiService.nodesApi.restoreNode(id)
+ )
+ .map(() => {
+ return {
+ id,
+ name,
+ status: 1
+ };
+ })
+ .catch((error: any) => {
+ return Observable.of({
+ id,
+ name,
+ status: 0
+ });
+ });
+ }
+
+ private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction {
+ if (status.someFailed && !status.oneFailed) {
+ return new SnackbarErrorAction(
+ 'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL',
+ { number: status.fail.length }
+ );
+ }
+
+ if (status.oneFailed) {
+ return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', {
+ name: status.fail[0].name
+ });
+ }
+
+ return null;
+ }
+
+ private purgeNodes(selection: NodeInfo[] = []) {
+ if (!selection.length) {
+ return;
+ }
+
+ const batch = selection.map(node => this.purgeDeletedNode(node));
+
+ Observable.forkJoin(batch).subscribe(purgedNodes => {
+ const status = this.processStatus(purgedNodes);
+
+ if (status.success.length) {
+ this.contentManagementService.nodesPurged.next();
+ }
+ const message = this.getPurgeMessage(status);
+ if (message) {
+ this.store.dispatch(message);
+ }
+ });
+ }
+
+ private purgeDeletedNode(node: NodeInfo): Observable {
+ const { id, name } = node;
+ const promise = this.alfrescoApiService.nodesApi.purgeDeletedNode(id);
+
+ return Observable.from(promise)
+ .map(() => ({
+ status: 1,
+ id,
+ name
+ }))
+ .catch(error => {
+ return Observable.of({
+ status: 0,
+ id,
+ name
+ });
+ });
+ }
+
+ private processStatus(data: DeletedNodeInfo[] = []): DeleteStatus {
+ 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 getPurgeMessage(status: DeleteStatus): SnackbarAction {
+ if (status.oneSucceeded && status.someFailed && !status.oneFailed) {
+ return new SnackbarWarningAction(
+ '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 new SnackbarWarningAction(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PARTIAL_PLURAL',
+ {
+ number: status.success.length,
+ failed: status.fail.length
+ }
+ );
+ }
+
+ if (status.oneSucceeded) {
+ return new SnackbarInfoAction(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.SINGULAR',
+ { name: status.success[0].name }
+ );
+ }
+
+ if (status.oneFailed) {
+ return new SnackbarErrorAction(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.SINGULAR',
+ { name: status.fail[0].name }
+ );
+ }
+
+ if (status.allSucceeded) {
+ return new SnackbarInfoAction(
+ 'APP.MESSAGES.INFO.TRASH.NODES_PURGE.PLURAL',
+ { number: status.success.length }
+ );
+ }
+
+ if (status.allFailed) {
+ return new SnackbarErrorAction(
+ 'APP.MESSAGES.ERRORS.TRASH.NODES_PURGE.PLURAL',
+ { number: status.fail.length }
+ );
+ }
+
+ return null;
+ }
+}
diff --git a/src/app/store/effects/router.effects.ts b/src/app/store/effects/router.effects.ts
new file mode 100644
index 000000000..b5d761cda
--- /dev/null
+++ b/src/app/store/effects/router.effects.ts
@@ -0,0 +1,18 @@
+import { Effect, Actions, ofType } from '@ngrx/effects';
+import { Injectable } from '@angular/core';
+import { NavigateRouteAction, NAVIGATE_ROUTE } from '../actions/router.action';
+import { map } from 'rxjs/operators';
+import { Router } from '@angular/router';
+
+@Injectable()
+export class RouterEffects {
+ constructor(private actions$: Actions, private router: Router) {}
+
+ @Effect({ dispatch: false })
+ navigateRoute$ = this.actions$.pipe(
+ ofType(NAVIGATE_ROUTE),
+ map(action => {
+ this.router.navigate(action.payload);
+ })
+ );
+}
diff --git a/src/app/store/effects/snackbar.effects.ts b/src/app/store/effects/snackbar.effects.ts
new file mode 100644
index 000000000..8d2c8f68d
--- /dev/null
+++ b/src/app/store/effects/snackbar.effects.ts
@@ -0,0 +1,74 @@
+import { Effect, Actions, ofType } from '@ngrx/effects';
+import { Injectable } from '@angular/core';
+import {
+ SnackbarErrorAction,
+ SNACKBAR_ERROR,
+ SNACKBAR_INFO,
+ SnackbarInfoAction,
+ SnackbarWarningAction,
+ SNACKBAR_WARNING,
+ SnackbarAction
+} from '../actions';
+import { MatSnackBar } from '@angular/material';
+import { TranslationService } from '@alfresco/adf-core';
+import { map } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { AppStore } from '../states/app.state';
+
+@Injectable()
+export class SnackbarEffects {
+ constructor(
+ private store: Store,
+ private actions$: Actions,
+ private snackBar: MatSnackBar,
+ private translationService: TranslationService
+ ) {}
+
+ @Effect({ dispatch: false })
+ infoEffect = this.actions$.pipe(
+ ofType(SNACKBAR_INFO),
+ map((action: SnackbarInfoAction) => {
+ this.showSnackBar(action, 'info-snackbar');
+ })
+ );
+
+ @Effect({ dispatch: false })
+ warningEffect = this.actions$.pipe(
+ ofType(SNACKBAR_WARNING),
+ map((action: SnackbarWarningAction) => {
+ this.showSnackBar(action, 'warning-snackbar');
+ })
+ );
+
+ @Effect({ dispatch: false })
+ errorEffect = this.actions$.pipe(
+ ofType(SNACKBAR_ERROR),
+ map((action: SnackbarErrorAction) => {
+ this.showSnackBar(action, 'error-snackbar');
+ })
+ );
+
+ private showSnackBar(action: SnackbarAction, panelClass: string) {
+ const message = this.translate(action.payload, action.params);
+
+ let actionName: string = null;
+ if (action.userAction) {
+ actionName = this.translate(action.userAction.title);
+ }
+
+ const snackBarRef = this.snackBar.open(message, actionName, {
+ duration: action.duration,
+ panelClass: panelClass
+ });
+
+ if (action.userAction) {
+ snackBarRef.onAction().subscribe(() => {
+ this.store.dispatch(action.userAction.action);
+ });
+ }
+ }
+
+ private translate(message: string, params?: Object): string {
+ return this.translationService.instant(message, params);
+ }
+}
diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts
index d12eae6ea..5e0d0ca2d 100644
--- a/src/app/store/reducers/app.reducer.ts
+++ b/src/app/store/reducers/app.reducer.ts
@@ -3,7 +3,7 @@ import { AppState, INITIAL_APP_STATE } from '../states/app.state';
import { SET_HEADER_COLOR, SetHeaderColorAction } from '../actions/header-color.action';
import { SET_APP_NAME, SetAppNameAction } from '../actions/app-name.action';
import { SET_LOGO_PATH, SetLogoPathAction } from '../actions/logo-path.action';
-import { SET_SELECTED_NODES, SetSelectedNodesAction } from '../actions/select-nodes.action';
+import { SET_SELECTED_NODES, SetSelectedNodesAction } from '../actions/node.action';
export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action): AppState {
diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss
index d84615b8f..1ed6bf486 100644
--- a/src/app/ui/custom-theme.scss
+++ b/src/app/ui/custom-theme.scss
@@ -3,6 +3,7 @@
@import '../components/sidenav/sidenav.component.theme';
@import './overrides/toolbar';
+@import 'snackbar';
$grey-scale: (
50 : #e0e0e0,
@@ -47,4 +48,5 @@ $custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent);
@mixin custom-theme($theme) {
@include sidenav-component-theme($custom-theme);
@include toolbar-component-theme($custom-theme);
+ @include snackbar-theme($custom-theme);
}
diff --git a/src/app/ui/snackbar.scss b/src/app/ui/snackbar.scss
new file mode 100644
index 000000000..929fe461f
--- /dev/null
+++ b/src/app/ui/snackbar.scss
@@ -0,0 +1,29 @@
+@mixin snackbar-theme($theme) {
+ $warn: map-get($theme, warn);
+ $accent: map-get($theme, accent);
+ $primary: map-get($theme, primary);
+
+ .error-snackbar {
+ background-color: mat-color($warn);
+
+ .mat-simple-snackbar-action {
+ color: white;
+ }
+ }
+
+ .warning-snackbar {
+ background-color: mat-color($accent);
+
+ .mat-simple-snackbar-action {
+ color: white;
+ }
+ }
+
+ .info-snackbar {
+ background-color: mat-color($primary);
+
+ .mat-simple-snackbar-action {
+ color: white;
+ }
+ }
+}