extensions: purge and delete toolbar actions (#528)

* rework "purge" action

* "restore deleted" action

* fix tests

* cleanup comments

* allow inline action names

* allow inline rules without params

* simplify bulk registration
This commit is contained in:
Denys Vuika
2018-07-23 09:39:06 +01:00
committed by GitHub
parent 98906942dc
commit 7509095d20
10 changed files with 565 additions and 422 deletions

View File

@@ -25,13 +25,9 @@
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 { AppStore } from '../store/states';
import { PurgeDeletedNodesAction } from '../store/actions';
import { NodeInfo } from '../store/models';
@Directive({
selector: '[acaPermanentDelete]'
@@ -43,35 +39,11 @@ export class NodePermanentDeleteDirective {
selection: MinimalNodeEntity[];
constructor(
private store: Store<AppStore>,
private dialog: MatDialog
private store: Store<AppStore>
) {}
@HostListener('click')
onClick() {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
title: 'APP.DIALOGS.CONFIRM_PURGE.TITLE',
message: 'APP.DIALOGS.CONFIRM_PURGE.MESSAGE',
yesLabel: 'APP.DIALOGS.CONFIRM_PURGE.YES_LABEL',
noLabel: 'APP.DIALOGS.CONFIRM_PURGE.NO_LABEL'
},
minWidth: '250px'
});
dialogRef.afterClosed().subscribe(result => {
if (result === true) {
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));
}
});
this.store.dispatch(new PurgeDeletedNodesAction(this.selection));
}
}

View File

@@ -28,7 +28,7 @@ import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testin
import { By } from '@angular/platform-browser';
import { NodeRestoreDirective } from './node-restore.directive';
import { ContentManagementService } from '../services/content-management.service';
import { Actions, ofType } from '@ngrx/effects';
import { Actions, ofType, EffectsModule } from '@ngrx/effects';
import { SnackbarErrorAction,
SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO,
NavigateRouteAction, NAVIGATE_ROUTE } from '../store/actions';
@@ -36,26 +36,30 @@ import { map } from 'rxjs/operators';
import { AppTestingModule } from '../testing/app-testing.module';
import { ContentApiService } from '../services/content-api.service';
import { Observable } from 'rxjs/Rx';
import { NodeEffects } from '../store/effects';
import { MinimalNodeEntity } from 'alfresco-js-api';
@Component({
template: `<div [acaRestoreNode]="selection"></div>`
})
class TestComponent {
selection = [];
selection: Array<MinimalNodeEntity> = [];
}
describe('NodeRestoreDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let component: TestComponent;
let directiveInstance: NodeRestoreDirective;
let contentManagementService: ContentManagementService;
let actions$: Actions;
let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ AppTestingModule ],
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects])
],
declarations: [
NodeRestoreDirective,
TestComponent
@@ -67,7 +71,6 @@ describe('NodeRestoreDirective', () => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
directiveInstance = element.injector.get(NodeRestoreDirective);
contentManagementService = TestBed.get(ContentManagementService);
contentApi = TestBed.get(ContentApiService);
@@ -96,13 +99,28 @@ describe('NodeRestoreDirective', () => {
});
it('call restore nodes if selection has nodes with path', fakeAsync(() => {
spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
list: { entries: [] }
}));
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
path
}
}
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
@@ -113,13 +131,28 @@ describe('NodeRestoreDirective', () => {
describe('refresh()', () => {
it('dispatch event on finish', fakeAsync(done => {
spyOn(directiveInstance, 'restoreNotification').and.callFake(() => null);
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
list: { entries: [] }
}));
component.selection = [{ entry: { id: '1', path: ['somewhere-over-the-rainbow'] } }];
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
path
}
}
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
@@ -158,10 +191,19 @@ describe('NodeRestoreDirective', () => {
}
});
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '3', name: 'name3', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } },
{ entry: { id: '2', name: 'name2', path } },
{ entry: { id: '3', name: 'name3', path } }
];
fixture.detectChanges();
@@ -178,8 +220,17 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
@@ -197,8 +248,17 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
@@ -216,8 +276,17 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
@@ -241,9 +310,18 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } },
{ entry: { id: '2', name: 'name2', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } },
{ entry: { id: '2', name: 'name2', path } }
];
fixture.detectChanges();
@@ -259,8 +337,17 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path: ['somewhere-over-the-rainbow'] } }
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
@@ -276,14 +363,21 @@ describe('NodeRestoreDirective', () => {
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
name: 'name1',
path: {
elements: ['somewhere-over-the-rainbow']
}
path
}
}
];

View File

@@ -24,25 +24,10 @@
*/
import { Directive, HostListener, Input } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import {
MinimalNodeEntity,
MinimalNodeEntryEntity,
PathInfoEntity,
DeletedNodesPaging
} from 'alfresco-js-api';
import { DeleteStatus, DeletedNodeInfo } from '../store/models';
import { ContentManagementService } from '../services/content-management.service';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state';
import {
NavigateRouteAction,
SnackbarAction,
SnackbarErrorAction,
SnackbarInfoAction,
SnackbarUserAction
} from '../store/actions';
import { ContentApiService } from '../services/content-api.service';
import { AppStore } from '../store/states';
import { RestoreDeletedNodesAction } from '../store/actions';
@Directive({
selector: '[acaRestoreNode]'
@@ -51,197 +36,10 @@ export class NodeRestoreDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaRestoreNode') selection: MinimalNodeEntity[];
constructor(private store: Store<AppStore>) {}
@HostListener('click')
onClick() {
this.restore(this.selection);
}
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private contentManagementService: ContentManagementService
) {}
private restore(selection: MinimalNodeEntity[] = []) {
if (!selection.length) {
return;
}
const nodesWithPath = selection.filter(node => node.entry.path);
if (selection.length && !nodesWithPath.length) {
const failedStatus = this.processStatus([]);
failedStatus.fail.push(...selection);
this.restoreNotification(failedStatus);
this.refresh();
return;
}
let status: DeleteStatus;
Observable.forkJoin(nodesWithPath.map(node => this.restoreNode(node)))
.do(restoredNodes => {
status = this.processStatus(restoredNodes);
})
.flatMap(() => this.contentApi.getDeletedNodes())
.subscribe((nodes: DeletedNodesPaging) => {
const selectedNodes = this.diff(status.fail, selection, false);
const remainingNodes = this.diff(
selectedNodes,
nodes.list.entries
);
if (!remainingNodes.length) {
this.restoreNotification(status);
this.refresh();
} else {
this.restore(remainingNodes);
}
});
}
private restoreNode(node: MinimalNodeEntity): Observable<any> {
const { entry } = node;
return this.contentApi.restoreNode(entry.id)
.map(() => ({
status: 1,
entry
}))
.catch(error => {
const { statusCode } = JSON.parse(error.message).error;
return Observable.of({
status: 0,
statusCode,
entry
});
});
}
private diff(selection, list, fromList = true): any {
const ids = selection.map(item => item.entry.id);
return list.filter(item => {
if (fromList) {
return ids.includes(item.entry.id) ? item : null;
} else {
return !ids.includes(item.entry.id) ? item : null;
}
});
}
private processStatus(data: 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 getRestoreMessage(status: DeleteStatus): SnackbarAction {
if (status.someFailed && !status.oneFailed) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL',
{ number: status.fail.length }
);
}
if (status.oneFailed && status.fail[0].statusCode) {
if (status.fail[0].statusCode === 409) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS',
{ name: status.fail[0].entry.name }
);
} else {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC',
{ name: status.fail[0].entry.name }
);
}
}
if (status.oneFailed && !status.fail[0].statusCode) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING',
{ name: status.fail[0].entry.name }
);
}
if (status.allSucceeded && !status.oneSucceeded) {
return new SnackbarInfoAction(
'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL'
);
}
if (status.allSucceeded && status.oneSucceeded) {
return new SnackbarInfoAction(
'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR',
{ 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 isSite = this.isSite(status.success[0].entry);
const path: PathInfoEntity = status.success[0].entry.path;
const parent = path.elements[path.elements.length - 1];
const route = isSite ? ['/libraries'] : ['/personal-files', parent.id];
const navigate = new NavigateRouteAction(route);
message.userAction = new SnackbarUserAction(
'APP.ACTIONS.VIEW',
navigate
);
}
this.store.dispatch(message);
}
}
private isSite(entry: MinimalNodeEntryEntity): boolean {
return entry.nodeType === 'st:site';
}
private refresh(): void {
this.contentManagementService.nodesRestored.next();
this.store.dispatch(new RestoreDeletedNodesAction(this.selection));
}
}

View File

@@ -30,36 +30,30 @@ import { LayoutComponent } from '../components/layout/layout.component';
import { TrashcanComponent } from '../components/trashcan/trashcan.component';
import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component';
import * as app from './evaluators/app.evaluators';
import * as core from './evaluators/core.evaluators';
import { ExtensionService } from './extension.service';
export function setupExtensions(extensions: ExtensionService): Function {
return () =>
new Promise(resolve => {
extensions
.setComponent('app.layout.main', LayoutComponent)
.setComponent('app.components.trashcan', TrashcanComponent)
.setAuthGuard('app.auth', AuthGuardEcm)
extensions.setComponents({
'app.layout.main': LayoutComponent,
'app.components.trashcan': TrashcanComponent
});
.setEvaluator('core.every', core.every)
.setEvaluator('core.some', core.some)
.setEvaluator('core.not', core.not)
.setEvaluator(
'app.selection.canDownload',
app.canDownloadSelection
)
.setEvaluator('app.selection.file', app.hasFileSelected)
.setEvaluator('app.selection.folder', app.hasFolderSelected)
.setEvaluator(
'app.selection.folder.canUpdate',
app.canUpdateSelectedFolder
)
.setEvaluator(
'app.navigation.folder.canCreate',
app.canCreateFolder
)
.setEvaluator('app.navigation.isTrashcan', app.isTrashcan)
.setEvaluator('app.navigation.isNotTrashcan', app.isNotTrashcan);
extensions.setAuthGuards({
'app.auth': AuthGuardEcm
});
extensions.setEvaluators({
'app.selection.canDownload': app.canDownloadSelection,
'app.selection.notEmpty': app.hasSelection,
'app.selection.file': app.hasFileSelected,
'app.selection.folder': app.hasFolderSelected,
'app.selection.folder.canUpdate': app.canUpdateSelectedFolder,
'app.navigation.folder.canCreate': app.canCreateFolder,
'app.navigation.isTrashcan': app.isTrashcan,
'app.navigation.isNotTrashcan': app.isNotTrashcan
});
resolve(true);
});

View File

@@ -35,6 +35,11 @@ export function isNotTrashcan(context: RuleContext, ...args: RuleParameter[]): b
return !isTrashcan(context, ...args);
}
export function hasSelection(context: RuleContext, ...args: RuleParameter[]): boolean {
const { selection } = context;
return selection && !selection.isEmpty;
}
export function canCreateFolder(context: RuleContext, ...args: RuleParameter[]): boolean {
const folder = context.navigation.currentFolder;
if (folder) {

View File

@@ -35,6 +35,7 @@ import { NavBarGroupRef } from './navbar.extensions';
import { RouteRef } from './routing.extensions';
import { RuleContext, RuleRef, RuleEvaluator } from './rule.extensions';
import { ActionRef, ContentActionRef, ContentActionType } from './action.extensions';
import * as core from './evaluators/core.evaluators';
@Injectable()
export class ExtensionService implements RuleContext {
@@ -63,6 +64,13 @@ export class ExtensionService implements RuleContext {
navigation: NavigationState;
constructor(private http: HttpClient, private store: Store<AppStore>) {
this.evaluators = {
'core.every': core.every,
'core.some': core.some,
'core.not': core.not
};
this.store.select(selectionWithFolder).subscribe(result => {
this.selection = result.selection;
this.navigation = result.navigation;
@@ -205,14 +213,22 @@ export class ExtensionService implements RuleContext {
return [];
}
setEvaluator(key: string, value: RuleEvaluator): ExtensionService {
this.evaluators[key] = value;
return this;
setEvaluators(values: { [key: string]: RuleEvaluator }) {
if (values) {
this.evaluators = Object.assign({}, this.evaluators, values);
}
}
setAuthGuard(key: string, value: Type<{}>): ExtensionService {
this.authGuards[key] = value;
return this;
setAuthGuards(values: { [key: string]: Type<{}> }) {
if (values) {
this.authGuards = Object.assign({}, this.authGuards, values);
}
}
setComponents(values: { [key: string]: Type<{}> }) {
if (values) {
this.components = Object.assign({}, this.components, values);
}
}
getRouteById(id: string): RouteRef {
@@ -229,11 +245,6 @@ export class ExtensionService implements RuleContext {
return this.navbar;
}
setComponent(id: string, value: Type<{}>): ExtensionService {
this.components[id] = value;
return this;
}
getComponentById(id: string): Type<{}> {
return this.components[id];
}
@@ -382,6 +393,8 @@ export class ExtensionService implements RuleContext {
const expression = this.runExpression(payload, context);
this.store.dispatch({ type, payload: expression });
} else {
this.store.dispatch({ type: id });
}
}
@@ -402,11 +415,17 @@ export class ExtensionService implements RuleContext {
evaluateRule(ruleId: string): boolean {
const ruleRef = this.rules.find(ref => ref.id === ruleId);
if (ruleRef) {
const evaluator = this.evaluators[ruleRef.type];
if (evaluator) {
return evaluator(this, ...ruleRef.parameters);
}
} else {
const evaluator = this.evaluators[ruleId];
if (evaluator) {
return evaluator(this);
}
}
return false;
}

View File

@@ -23,21 +23,32 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Subject } from 'rxjs/Rx';
import { Subject, Observable } from 'rxjs/Rx';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';
import { FolderDialogComponent } from '@alfresco/adf-content-services';
import { FolderDialogComponent, ConfirmDialogComponent } from '@alfresco/adf-content-services';
import { LibraryDialogComponent } from '../dialogs/library/library.dialog';
import { SnackbarErrorAction } from '../store/actions';
import { SnackbarErrorAction, SnackbarInfoAction, SnackbarAction, SnackbarWarningAction,
NavigateRouteAction, SnackbarUserAction } from '../store/actions';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states';
import {
MinimalNodeEntity,
MinimalNodeEntryEntity,
Node,
SiteEntry
SiteEntry,
DeletedNodesPaging,
PathInfoEntity
} from 'alfresco-js-api';
import { NodePermissionService } from './node-permission.service';
import { NodeInfo, DeletedNodeInfo, DeleteStatus } from '../store/models';
import { ContentApiService } from './content-api.service';
interface RestoredNode {
status: number;
entry: MinimalNodeEntryEntity;
statusCode?: number;
}
@Injectable()
export class ContentManagementService {
@@ -53,6 +64,7 @@ export class ContentManagementService {
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private permission: NodePermissionService,
private dialogRef: MatDialog
) {}
@@ -144,4 +156,305 @@ export class ContentManagementService {
target: 'allowableOperationsOnTarget'
});
}
purgeDeletedNodes(nodes: MinimalNodeEntity[]) {
if (!nodes || nodes.length === 0) {
return;
}
const dialogRef = this.dialogRef.open(ConfirmDialogComponent, {
data: {
title: 'APP.DIALOGS.CONFIRM_PURGE.TITLE',
message: 'APP.DIALOGS.CONFIRM_PURGE.MESSAGE',
yesLabel: 'APP.DIALOGS.CONFIRM_PURGE.YES_LABEL',
noLabel: 'APP.DIALOGS.CONFIRM_PURGE.NO_LABEL'
},
minWidth: '250px'
});
dialogRef.afterClosed().subscribe(result => {
if (result === true) {
const nodesToDelete: NodeInfo[] = nodes.map(node => {
const { name } = node.entry;
const id = node.entry.nodeId || node.entry.id;
return {
id,
name
};
});
this.purgeNodes(nodesToDelete);
}
});
}
restoreDeletedNodes(selection: MinimalNodeEntity[] = []) {
if (!selection.length) {
return;
}
const nodesWithPath = selection.filter(node => node.entry.path);
if (selection.length && !nodesWithPath.length) {
const failedStatus = this.processStatus([]);
failedStatus.fail.push(...selection);
this.showRestoreNotification(failedStatus);
this.nodesRestored.next();
return;
}
let status: DeleteStatus;
Observable.forkJoin(nodesWithPath.map(node => this.restoreNode(node)))
.do(restoredNodes => {
status = this.processStatus(restoredNodes);
})
.flatMap(() => this.contentApi.getDeletedNodes())
.subscribe((nodes: DeletedNodesPaging) => {
const selectedNodes = this.diff(status.fail, selection, false);
const remainingNodes = this.diff(
selectedNodes,
nodes.list.entries
);
if (!remainingNodes.length) {
this.showRestoreNotification(status);
this.nodesRestored.next();
} else {
this.restoreDeletedNodes(remainingNodes);
}
});
}
private restoreNode(node: MinimalNodeEntity): Observable<RestoredNode> {
const { entry } = node;
return this.contentApi.restoreNode(entry.id)
.map(() => ({
status: 1,
entry
}))
.catch(error => {
const { statusCode } = JSON.parse(error.message).error;
return Observable.of({
status: 0,
statusCode,
entry
});
});
}
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.nodesPurged.next();
}
const message = this.getPurgeMessage(status);
if (message) {
this.store.dispatch(message);
}
});
}
private purgeDeletedNode(node: NodeInfo): Observable<DeletedNodeInfo> {
const { id, name } = node;
return this.contentApi
.purgeDeletedNode(id)
.map(() => ({
status: 1,
id,
name
}))
.catch(error => {
return Observable.of({
status: 0,
id,
name
});
});
}
private processStatus(data: Array<{ status: number }> = []): 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;
}
private showRestoreNotification(status: DeleteStatus): void {
const message = this.getRestoreMessage(status);
if (message) {
if (status.oneSucceeded && !status.someFailed) {
const isSite = this.isSite(status.success[0].entry);
const path: PathInfoEntity = status.success[0].entry.path;
const parent = path.elements[path.elements.length - 1];
const route = isSite ? ['/libraries'] : ['/personal-files', parent.id];
const navigate = new NavigateRouteAction(route);
message.userAction = new SnackbarUserAction(
'APP.ACTIONS.VIEW',
navigate
);
}
this.store.dispatch(message);
}
}
private isSite(entry: MinimalNodeEntryEntity): boolean {
return entry.nodeType === 'st:site';
}
private getRestoreMessage(status: DeleteStatus): SnackbarAction {
if (status.someFailed && !status.oneFailed) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL',
{ number: status.fail.length }
);
}
if (status.oneFailed && status.fail[0].statusCode) {
if (status.fail[0].statusCode === 409) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS',
{ name: status.fail[0].entry.name }
);
} else {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC',
{ name: status.fail[0].entry.name }
);
}
}
if (status.oneFailed && !status.fail[0].statusCode) {
return new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING',
{ name: status.fail[0].entry.name }
);
}
if (status.allSucceeded && !status.oneSucceeded) {
return new SnackbarInfoAction(
'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL'
);
}
if (status.allSucceeded && status.oneSucceeded) {
return new SnackbarInfoAction(
'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR',
{ name: status.success[0].entry.name }
);
}
return null;
}
private diff(selection, list, fromList = true): any {
const ids = selection.map(item => item.entry.id);
return list.filter(item => {
if (fromList) {
return ids.includes(item.entry.id) ? item : null;
} else {
return !ids.includes(item.entry.id) ? item : null;
}
});
}
}

View File

@@ -53,12 +53,12 @@ export class UndoDeleteNodesAction implements Action {
export class RestoreDeletedNodesAction implements Action {
readonly type = RESTORE_DELETED_NODES;
constructor(public payload: any[] = []) {}
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class PurgeDeletedNodesAction implements Action {
readonly type = PURGE_DELETED_NODES;
constructor(public payload: NodeInfo[] = []) {}
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class DownloadNodesAction implements Action {

View File

@@ -48,7 +48,7 @@ import { Observable } from 'rxjs/Rx';
import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models';
import { ContentApiService } from '../../services/content-api.service';
import { currentFolder, appSelection } from '../selectors/app.selectors';
import { EditFolderAction, EDIT_FOLDER } from '../actions/node.actions';
import { EditFolderAction, EDIT_FOLDER, RestoreDeletedNodesAction, RESTORE_DELETED_NODES } from '../actions/node.actions';
@Injectable()
export class NodeEffects {
@@ -63,7 +63,37 @@ export class NodeEffects {
purgeDeletedNodes$ = this.actions$.pipe(
ofType<PurgeDeletedNodesAction>(PURGE_DELETED_NODES),
map(action => {
this.purgeNodes(action.payload);
if (action && action.payload && action.payload.length > 0) {
this.contentManagementService.purgeDeletedNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.count > 0) {
this.contentManagementService.purgeDeletedNodes(selection.nodes);
}
});
}
})
);
@Effect({ dispatch: false })
restoreDeletedNodes$ = this.actions$.pipe(
ofType<RestoreDeletedNodesAction>(RESTORE_DELETED_NODES),
map(action => {
if (action && action.payload && action.payload.length > 0) {
this.contentManagementService.restoreDeletedNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.count > 0) {
this.contentManagementService.restoreDeletedNodes(selection.nodes);
}
});
}
})
);
@@ -284,45 +314,6 @@ export class NodeEffects {
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<DeletedNodeInfo> {
const { id, name } = node;
return this.contentApi
.purgeDeletedNode(id)
.map(() => ({
status: 1,
id,
name
}))
.catch(error => {
return Observable.of({
status: 0,
id,
name
});
});
}
private processStatus(data: DeletedNodeInfo[] = []): DeleteStatus {
const status = {
fail: [],
@@ -361,56 +352,4 @@ export class NodeEffects {
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;
}
}

View File

@@ -9,8 +9,12 @@
"rules": [
{
"id": "app.create.canCreateFolder",
"type": "app.navigation.folder.canCreate"
"id": "app.trashcan.hasSelection",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isTrashcan" }
]
},
{
"id": "app.toolbar.canEditFolder",
@@ -47,25 +51,6 @@
}
],
"actions": [
{
"id": "app.actions.createFolder",
"type": "CREATE_FOLDER"
},
{
"id": "app.actions.editFolder",
"type": "EDIT_FOLDER"
},
{
"id": "app.actions.download",
"type": "DOWNLOAD_NODES"
},
{
"id": "app.actions.preview",
"type": "VIEW_FILE"
}
],
"features": {
"create": [
{
@@ -74,10 +59,10 @@
"icon": "create_new_folder",
"title": "ext: Create Folder",
"actions": {
"click": "app.actions.createFolder"
"click": "CREATE_FOLDER"
},
"rules": {
"enabled": "app.create.canCreateFolder"
"enabled": "app.navigation.folder.canCreate"
}
}
],
@@ -149,10 +134,10 @@
"title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
"icon": "create_new_folder",
"actions": {
"click": "app.actions.createFolder"
"click": "CREATE_FOLDER"
},
"rules": {
"visible": "app.create.canCreateFolder"
"visible": "app.navigation.folder.canCreate"
}
},
{
@@ -162,7 +147,7 @@
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
"actions": {
"click": "app.actions.preview"
"click": "VIEW_FILE"
},
"rules": {
"visible": "app.toolbar.canViewFile"
@@ -175,7 +160,7 @@
"title": "APP.ACTIONS.DOWNLOAD",
"icon": "get_app",
"actions": {
"click": "app.actions.download"
"click": "DOWNLOAD_NODES"
},
"rules": {
"visible": "app.toolbar.canDownload"
@@ -188,12 +173,36 @@
"title": "APP.ACTIONS.EDIT",
"icon": "create",
"actions": {
"click": "app.actions.editFolder"
"click": "EDIT_FOLDER"
},
"rules": {
"visible": "app.toolbar.canEditFolder"
}
},
{
"id": "app.toolbar.purgeDeletedNodes",
"type": "button",
"title": "APP.ACTIONS.DELETE_PERMANENT",
"icon": "delete_forever",
"actions": {
"click": "PURGE_DELETED_NODES"
},
"rules": {
"visible": "app.trashcan.hasSelection"
}
},
{
"id": "app.toolbar.restoreDeletedNodes",
"type": "button",
"title": "APP.ACTIONS.RESTORE",
"icon": "restore",
"actions": {
"click": "RESTORE_DELETED_NODES"
},
"rules": {
"visible": "app.trashcan.hasSelection"
}
},
{
"id": "app.toolbar.separator.2",
"order": 200,