[ACA-1631] more application ngrx actions (#540)

* delete action

* library path evaluator

* extension for sharing files

* upload actions

* delete library

* use extensions for experimental library actions

* unshare nodes

* fix icons and titles

* "create menu" backed by core extension

* support for descriptions, update upload selector

* update code and tests

* support disabled tooltips for navbar

* fix selector

* [ACA-1486] remove double fetch call

* migrate to trashcan actions, element IDs

* cleanup code, remove deprecated directives

* add/remove favorite

* improve rendering performance

* update favorites without reload

* support for adding Sites to favorites

* disable favorites for Libraries for now

* copy action

* move node

* manage versions and permissions

* cleanup code

* toggle info drawer

* card view mode

* use extension layer for favorites toolbar

* fix menu tooltips

* fix 'remove as favorite' tests

* update tests

* test fixes

* fix edit folder for favorites

* fix test

* cleanup favorites layout

* upgrade recent files layout

* update evaluators for shared nodes

* test fixes

* test fixes

* restore recent files layout

* workaround for "favorite" toggle and recent files

* upgrade shared files page

* upgrade files page layout

* fix library evaluator

* workaround for shared files and permissions

* cleanup code

* upgrade search results

* upgrade sidebar and viewer actions

* code cleanup

* code cleanup

* code cleanup
This commit is contained in:
Denys Vuika 2018-07-31 10:36:26 +01:00 committed by GitHub
parent 617f80c9fd
commit ae8675dfd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 3578 additions and 3789 deletions

View File

@ -32,12 +32,12 @@ export class Menu extends Component {
root: '.mat-menu-panel',
item: '.mat-menu-item',
icon: '.mat-icon',
uploadFiles: 'input[id="upload-multiple-files"]'
uploadFiles: 'app-upload-files'
};
items: ElementArrayFinder = this.component.all(by.css(Menu.selectors.item));
backdrop: ElementFinder = browser.element(by.css('.cdk-overlay-backdrop'));
uploadFiles: ElementFinder = this.component.element(by.css(Menu.selectors.uploadFiles));
uploadFiles: ElementFinder = browser.element(by.id(Menu.selectors.uploadFiles));
constructor(ancestor?: ElementFinder) {
super(Menu.selectors.root, ancestor);

View File

@ -155,7 +155,7 @@ describe('Create folder', () => {
.then(() => menu))
.then(menu => {
const tooltip = menu.getItemTooltip('Create folder');
expect(tooltip).toContain(`You can't create a folder here`);
expect(tooltip).toContain(`Folders cannot be created whilst viewing the current items.`);
});
});

View File

@ -115,6 +115,14 @@
"description": "Element title",
"type": "string"
},
"description": {
"description": "Element description, used for the tooltips.",
"type": "string"
},
"description-disabled": {
"description": "Description to use when element is in the disabled state.",
"type": "string"
},
"order": {
"description": "Element order",
"type": "number"
@ -283,6 +291,12 @@
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
},
"actions": {
"description": "Content actions (toolbar, context menus, etc.)",
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
}
}
},

View File

@ -9,7 +9,6 @@
"© 2017 - 2018 Alfresco Software, Inc. All rights reserved."
},
"experimental": {
"libraries": false,
"comments": false,
"cardview": false,
"permissions": false,

View File

@ -73,6 +73,9 @@ import { ViewUtilService} from './services/view-util.service';
import { ExtensionService } from './extensions/extension.service';
import { AppInfoDrawerModule } from './components/info-drawer/info.drawer.module';
import { DirectivesModule } from './directives/directives.module';
import { ToggleInfoDrawerComponent } from './components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
import { DocumentDisplayModeComponent } from './components/toolbar/document-display-mode/document-display-mode.component';
import { ToggleFavoriteComponent } from './components/toolbar/toggle-favorite/toggle-favorite.component';
export function setupExtensionServiceFactory(service: ExtensionService): Function {
return () => service.load();
@ -121,7 +124,10 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio
PermissionsManagerComponent,
SearchResultsComponent,
SettingsComponent,
SharedLinkViewComponent
SharedLinkViewComponent,
ToggleInfoDrawerComponent,
DocumentDisplayModeComponent,
ToggleFavoriteComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
@ -151,7 +157,10 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio
entryComponents: [
LibraryDialogComponent,
NodeVersionsDialogComponent,
NodePermissionsDialogComponent
NodePermissionsDialogComponent,
ToggleInfoDrawerComponent,
DocumentDisplayModeComponent,
ToggleFavoriteComponent
],
bootstrap: [AppComponent]
})

View File

@ -4,129 +4,17 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
color="primary"
mat-icon-button
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
mat-icon-button
color="primary"
*ngIf="selection.file"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(selection.file)">
<mat-icon>open_in_browser</mat-icon>
</button>
<button
mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
(click)="downloadSelection()">
<mat-icon>get_app</mat-icon>
</button>
<button
mat-icon-button
color="primary"
*ngIf="selection.folder"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
[acaEditFolder]="selection.folder">
<mat-icon>create</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<ng-container *ifExperimental="'share'">
<button mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
*ngIf="selection.file"
[baseShareUrl]="sharedPreviewUrl$ | async"
[adf-share]="selection.file">
<mat-icon>share</mat-icon>
</button>
</ng-container>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false">
<button
mat-menu-item
#favorites="adfFavorite"
(toggle)="reload()"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
[acaDeleteNode]="selection.nodes">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="selection.file"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="selection.count === 1"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
</ng-container>
</mat-menu>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<div class="inner-layout__panel">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
currentFolderId="-favorites-"
selectionMode="multiple"
[navigate]="false"
@ -200,7 +88,7 @@
</adf-pagination>
</div>
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</div>

View File

@ -59,7 +59,8 @@ export class FavoritesComponent extends PageComponent implements OnInit {
this.content.nodesDeleted.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()),
this.content.folderEdited.subscribe(() => this.reload()),
this.content.nodesMoved.subscribe(() => this.reload())
this.content.nodesMoved.subscribe(() => this.reload()),
this.content.favoriteRemoved.subscribe(() => this.reload())
]);
}

View File

@ -7,125 +7,9 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
color="primary"
mat-icon-button
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
color="primary"
mat-icon-button
*ngIf="selection.file"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(selection.file)">
<mat-icon>open_in_browser</mat-icon>
</button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
(click)="downloadSelection()">
<mat-icon>get_app</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="canEditFolder"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
[acaEditFolder]="selection.folder">
<mat-icon>create</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<ng-container *ifExperimental="'share'">
<button mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
*ngIf="selection.file"
[baseShareUrl]="sharedPreviewUrl$ | async"
[adf-share]="selection.file">
<mat-icon>share</mat-icon>
</button>
</ng-container>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false">
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaDeleteNode]="selection.nodes">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="selection.file"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="canUpdateNode"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
</ng-container>
</mat-menu>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
@ -141,6 +25,7 @@
[disabled]="!canUpload">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
[sorting]="[ 'modifiedAt', 'desc' ]"
selectionMode="multiple"
[currentFolderId]="node?.id"
@ -199,7 +84,7 @@
</adf-upload-drag-area>
</div>
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</div>

View File

@ -29,7 +29,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import {
TimeAgoPipe, NodeNameTooltipPipe, FileSizePipe, NodeFavoriteDirective,
DataTableComponent, UploadService, AppConfigPipe
DataTableComponent,
UploadService,
AppConfigPipe
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service';
@ -41,7 +43,6 @@ import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('FilesComponent', () => {
let node;
let page;
let fixture: ComponentFixture<FilesComponent>;
let component: FilesComponent;
let contentManagementService: ContentManagementService;
@ -90,20 +91,12 @@ describe('FilesComponent', () => {
beforeEach(() => {
node = { id: 'node-id', isFolder: true };
page = {
list: {
entries: ['a', 'b', 'c'],
pagination: {}
}
};
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
});
describe('Current page is valid', () => {
it('should be a valid current page', fakeAsync(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component, 'fetchNodes').and.returnValue(Observable.throw(null));
spyOn(contentApi, 'getNode').and.returnValue(Observable.throw(null));
component.ngOnInit();
fixture.detectChanges();
@ -114,7 +107,6 @@ describe('FilesComponent', () => {
it('should set current page as invalid path', fakeAsync(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
component.ngOnInit();
tick();
@ -127,22 +119,10 @@ describe('FilesComponent', () => {
describe('OnInit', () => {
it('should set current node', () => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
fixture.detectChanges();
expect(component.node).toBe(node);
});
it('should get current node children', () => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
fixture.detectChanges();
expect(component.fetchNodes).toHaveBeenCalled();
});
it('if should navigate to parent if node is not a folder', () => {
node.isFolder = false;
node.parentId = 'parent-id';
@ -157,8 +137,7 @@ describe('FilesComponent', () => {
describe('refresh on events', () => {
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(component.documentList, 'reload');
fixture.detectChanges();
@ -170,9 +149,9 @@ describe('FilesComponent', () => {
{ entry: { parentId: '2' } }
];
component.node = <any>{ id: '1' };
component.node = { id: '1' };
nodeActionsService.contentCopied.next(<any>nodes);
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).toHaveBeenCalled();
});
@ -183,9 +162,9 @@ describe('FilesComponent', () => {
{ entry: { parentId: '2' } }
];
component.node = <any>{ id: '3' };
component.node = { id: '3' };
nodeActionsService.contentCopied.next(<any>nodes);
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).not.toHaveBeenCalled();
});
@ -222,7 +201,7 @@ describe('FilesComponent', () => {
it('should call refresh on fileUploadComplete event if parent node match', () => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = <any>{ id: 'parentId' };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
@ -231,7 +210,7 @@ describe('FilesComponent', () => {
it('should not call refresh on fileUploadComplete event if parent mismatch', () => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = <any>{ id: 'parentId' };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
@ -240,7 +219,7 @@ describe('FilesComponent', () => {
it('should call refresh on fileUploadDeleted event if parent node match', () => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = <any>{ id: 'parentId' };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>file);
@ -248,40 +227,24 @@ describe('FilesComponent', () => {
});
it('should not call refresh on fileUploadDeleted event if parent mismatch', () => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = <any>{ id: 'parentId' };
const file: any = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>file);
uploadService.fileUploadDeleted.next(file);
expect(component.documentList.reload).not.toHaveBeenCalled();
});
});
describe('fetchNodes()', () => {
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
spyOn(contentApi, 'getNodeChildren').and.returnValue(Observable.of(page));
fixture.detectChanges();
});
it('should call getNode api with node id', () => {
component.fetchNodes('nodeId');
expect(contentApi.getNodeChildren).toHaveBeenCalledWith('nodeId');
});
});
describe('onBreadcrumbNavigate()', () => {
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
fixture.detectChanges();
});
it('should navigates to node id', () => {
const routeData = <any>{ id: 'some-where-over-the-rainbow' };
const routeData: any = { id: 'some-where-over-the-rainbow' };
spyOn(component, 'navigate');
component.onBreadcrumbNavigate(routeData);
@ -292,8 +255,7 @@ describe('FilesComponent', () => {
describe('Node navigation', () => {
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges();
@ -312,7 +274,7 @@ describe('FilesComponent', () => {
});
it('should navigate home if node is root', () => {
(<any>component).node = {
component.node = {
path: {
elements: [ {id: 'node-id'} ]
}

View File

@ -27,8 +27,7 @@ import { FileUploadEvent, UploadService } from '@alfresco/adf-core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Rx';
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElement, PathElementEntity } from 'alfresco-js-api';
import { ContentManagementService } from '../../services/content-management.service';
import { NodeActionsService } from '../../services/node-actions.service';
import { AppStore } from '../../store/states/app.state';
@ -68,19 +67,21 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
route.params.subscribe(({ folderId }: Params) => {
const nodeId = folderId || data.defaultNodeId;
this.contentApi.getNode(nodeId)
.map(node => node.entry)
.do(node => {
if (node.isFolder) {
this.updateCurrentNode(node);
} else {
this.router.navigate(['/personal-files', node.parentId], { replaceUrl: true });
}
})
.skipWhile(node => !node.isFolder)
.flatMap(node => this.fetchNodes(node.id))
this.contentApi
.getNode(nodeId)
.subscribe(
() => this.isValidPath = true,
node => {
this.isValidPath = true;
if (node.entry && node.entry.isFolder) {
this.updateCurrentNode(node.entry);
} else {
this.router.navigate(
['/personal-files', node.entry.parentId],
{ replaceUrl: true }
);
}
},
() => this.isValidPath = false
);
});
@ -102,10 +103,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.store.dispatch(new SetCurrentFolderAction(null));
}
fetchNodes(parentNodeId?: string): Observable<NodePaging> {
return this.contentApi.getNodeChildren(parentNodeId);
}
navigate(nodeId: string = null) {
const commands = [ './' ];

View File

@ -4,39 +4,11 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
mat-icon-button
color="primary"
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<button
mat-icon-button
color="primary"
*ifExperimental="'libraries'"
(click)="createLibrary()">
<mat-icon>create_new_folder</mat-icon>
</button>
<ng-container *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'libraries'">
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu" [overlapTrigger]="false">
<button
mat-menu-item
(click)="deleteLibrary(selection.first)">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
</mat-menu>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
</adf-toolbar>
@ -45,6 +17,7 @@
<div class="inner-layout__content">
<div class="inner-layout__panel">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
currentFolderId="-mysites-"
selectionMode="single"
[navigate]="false"

View File

@ -30,7 +30,6 @@ import { ShareDataRow } from '@alfresco/adf-content-services';
import { PageComponent } from '../page.component';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state';
import { DeleteLibraryAction, CreateLibraryAction } from '../../store/actions';
import { SiteEntry } from 'alfresco-js-api';
import { ContentManagementService } from '../../services/content-management.service';
import { ContentApiService } from '../../services/content-api.service';
@ -100,14 +99,4 @@ export class LibrariesComponent extends PageComponent implements OnInit {
});
}
}
deleteLibrary(node: SiteEntry) {
if (node && node.entry) {
this.store.dispatch(new DeleteLibraryAction(node.entry.id));
}
}
createLibrary() {
this.store.dispatch(new CreateLibraryAction());
}
}

View File

@ -24,14 +24,13 @@
*/
import { DocumentListComponent, ShareDataRow } from '@alfresco/adf-content-services';
import { DisplayMode } from '@alfresco/adf-core';
import { OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
import { takeUntil } from 'rxjs/operators';
import { Subject, Subscription } from 'rxjs/Rx';
import { SetSelectedNodesAction, DownloadNodesAction, ViewFileAction } from '../store/actions';
import { appSelection, sharedUrl, currentFolder } from '../store/selectors/app.selectors';
import { SetSelectedNodesAction, ViewFileAction } from '../store/actions';
import { appSelection, sharedUrl, currentFolder, infoDrawerOpened, documentDisplayMode } from '../store/selectors/app.selectors';
import { AppStore } from '../store/states/app.state';
import { SelectionState } from '../store/states/selection.state';
import { Observable } from 'rxjs/Rx';
@ -47,19 +46,15 @@ export abstract class PageComponent implements OnInit, OnDestroy {
documentList: DocumentListComponent;
title = 'Page';
infoDrawerOpened = false;
infoDrawerOpened$: Observable<boolean>;
node: MinimalNodeEntryEntity;
selection: SelectionState;
displayMode = DisplayMode.List;
documentDisplayMode$: Observable<string>;
sharedPreviewUrl$: Observable<string>;
actions: Array<ContentActionRef> = [];
canUpdateFile = false;
viewerActions: Array<ContentActionRef> = [];
canUpdateNode = false;
canDelete = false;
canEditFolder = false;
canUpload = false;
canDeleteShared = false;
canUpdateShared = false;
protected subscriptions: Subscription[] = [];
@ -74,22 +69,17 @@ export abstract class PageComponent implements OnInit, OnDestroy {
ngOnInit() {
this.sharedPreviewUrl$ = this.store.select(sharedUrl);
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
this.documentDisplayMode$ = this.store.select(documentDisplayMode);
this.store
.select(appSelection)
.pipe(takeUntil(this.onDestroy$))
.subscribe(selection => {
this.selection = selection;
if (selection.isEmpty) {
this.infoDrawerOpened = false;
}
this.actions = this.extensions.getAllowedContentActions();
this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file);
this.viewerActions = this.extensions.getViewerActions();
this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first);
this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes);
this.canEditFolder = selection.folder && this.content.canUpdateNode(selection.folder);
this.canDeleteShared = !this.selection.isEmpty && this.content.canDeleteSharedNodes(selection.nodes);
this.canUpdateShared = selection.file && this.content.canUpdateSharedNode(selection.file);
});
this.store.select(currentFolder)
@ -127,14 +117,6 @@ export abstract class PageComponent implements OnInit, OnDestroy {
return null;
}
toggleSidebar(event) {
if (event) {
return;
}
this.infoDrawerOpened = !this.infoDrawerOpened;
}
reload(): void {
if (this.documentList) {
this.documentList.resetSelection();
@ -143,22 +125,7 @@ export abstract class PageComponent implements OnInit, OnDestroy {
}
}
toggleGalleryView(): void {
this.displayMode = this.displayMode === DisplayMode.List ? DisplayMode.Gallery : DisplayMode.List;
this.documentList.display = this.displayMode;
}
downloadSelection() {
this.store.dispatch(new DownloadNodesAction());
}
// this is where each application decides how to treat an action and what to do
// the ACA maps actions to the NgRx actions as an example
runAction(actionId: string) {
const context = {
selection: this.selection
};
this.extensions.runActionById(actionId, context);
trackByActionId(index: number, action: ContentActionRef) {
return action.id;
}
}

View File

@ -8,7 +8,7 @@
[canNavigateBefore]="previousNodeId"
[canNavigateNext]="nextNodeId"
[overlayMode]="true"
(print) = "printFile($event)"
(print)="printFile()"
(showViewerChange)="onVisibilityChanged($event)"
(navigateBefore)="onNavigateBefore()"
(navigateNext)="onNavigateNext()">
@ -18,74 +18,14 @@
</adf-viewer-sidebar>
<adf-viewer-open-with *ifExperimental="'extensions'">
<button *ngFor="let entry of openWith"
mat-menu-item
(click)="runAction(entry.actions.click)">
<mat-icon>{{ entry.icon }}</mat-icon>
<span>{{ entry.title }}</span>
</button>
<ng-container *ngFor="let action of openWith; trackBy: trackByActionId">
<aca-toolbar-action type="menu-item" [entry]="action"></aca-toolbar-action>
</ng-container>
</adf-viewer-open-with>
<adf-viewer-more-actions>
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<ng-container *ifExperimental="'share'">
<button mat-menu-item
color="primary"
[baseShareUrl]="sharedPreviewUrl$ | async"
[adf-share]="selection.file">
<mat-icon>share</mat-icon>
<span>{{ 'APP.ACTIONS.SHARE' | translate }}</span>
</button>
</ng-container>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
(click)="deleteFile()">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canUpdateFile"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="canUpdateNode"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
<ng-container *ngFor="let action of viewerActions; trackBy: trackByActionId">
<aca-toolbar-action type="menu-item" [entry]="action"></aca-toolbar-action>
</ng-container>
</adf-viewer-more-actions>
</adf-viewer>

View File

@ -28,7 +28,7 @@ import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_O
import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state';
import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions';
import { SetSelectedNodesAction } from '../../store/actions';
import { PageComponent } from '../page.component';
import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service';
@ -335,17 +335,7 @@ export class PreviewComponent extends PageComponent implements OnInit {
return path;
}
deleteFile() {
this.store.dispatch(new DeleteNodesAction([
{
id: this.node.nodeId || this.node.id,
name: this.node.name
}
]));
this.onVisibilityChanged(false);
}
printFile(event: any) {
printFile() {
this.viewUtils.printFileGeneric(this.nodeId, this.node.content.mimeType);
}

View File

@ -27,7 +27,7 @@ import { CoreModule } from '@alfresco/adf-core';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
import { DirectivesModule } from '../../directives/directives.module';
import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module';
import { PreviewComponent } from './preview.component';
@ -51,7 +51,8 @@ const routes: Routes = [
CoreModule.forChild(),
ContentDirectiveModule,
DirectivesModule,
AppInfoDrawerModule
AppInfoDrawerModule,
CoreExtensionsModule.forChild()
],
declarations: [
PreviewComponent,

View File

@ -4,116 +4,10 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
color="primary"
mat-icon-button
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
mat-icon-button
color="primary"
*ngIf="selection.file"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(selection.file)">
<mat-icon>open_in_browser</mat-icon>
</button>
<button
mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
(click)="downloadSelection()">
<mat-icon>get_app</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<ng-container *ifExperimental="'share'">
<button *ngIf="selection.file"
mat-icon-button
color="primary"
title="{{ 'APP.ACTIONS.SHARE' | translate }}"
[baseShareUrl]="sharedPreviewUrl$ | async"
[adf-share]="selection.file">
<mat-icon>share</mat-icon>
</button>
</ng-container>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false">
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaDeleteNode]="selection.nodes">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="selection.file"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="canUpdateNode"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
</ng-container>
</mat-menu>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
@ -121,6 +15,7 @@
<div class="inner-layout__content">
<div class="inner-layout__panel">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
currentFolderId="-recent-"
selectionMode="multiple"
[navigate]="false"
@ -188,7 +83,7 @@
</adf-pagination>
</div>
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</div>

View File

@ -3,80 +3,8 @@
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE">
</adf-breadcrumb>
<adf-toolbar class="inline">
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
color="primary"
mat-icon-button
*ngIf="selection.file"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(selection.file)">
<mat-icon>open_in_browser</mat-icon>
</button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
(click)="downloadSelection()">
<mat-icon>get_app</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu" [overlapTrigger]="false">
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="selection.file"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="canUpdateNode"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
</ng-container>
</mat-menu>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
@ -154,7 +82,7 @@
</div>
</div>
</div>
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</div>

View File

@ -4,113 +4,10 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
color="primary"
mat-icon-button
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
*ngIf="selection.count === 1"
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="showPreview(selection.nodes[0])">
<mat-icon>open_in_browser</mat-icon>
</button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
(click)="downloadSelection()">
<mat-icon>get_app</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false">
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="selection.nodes">
<mat-icon color="primary" *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
<button
mat-menu-item
[acaCopyNode]="selection.nodes">
<mat-icon>content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDeleteShared"
[acaMoveNode]="selection.nodes">
<mat-icon>library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete"
[acaUnshareNode]="selection.nodes">
<mat-icon>stop_screen_share</mat-icon>
<span>{{ 'APP.ACTIONS.UNSHARE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDeleteShared"
[acaDeleteNode]="selection.nodes">
<mat-icon>delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canUpdateShared"
[acaNodeVersions]="selection.file">
<mat-icon>history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
<ng-container *ifExperimental="'permissions'">
<button
mat-menu-item
*ngIf="canUpdateShared"
[acaNodePermissions]="selection.first">
<mat-icon>settings_input_component</mat-icon>
<span>{{ 'APP.ACTIONS.PERMISSIONS' | translate }}</span>
</button>
</ng-container>
</mat-menu>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
@ -118,6 +15,7 @@
<div class="inner-layout__content">
<div class="inner-layout__panel">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
currentFolderId="-sharedlinks-"
selectionMode="multiple"
[sorting]="[ 'modifiedAt', 'desc' ]"
@ -196,7 +94,7 @@
</adf-pagination>
</div>
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened">
<div class="inner-layout__side-panel" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last"></aca-info-drawer>
</div>
</div>

View File

@ -1,75 +1,39 @@
<div class="sidenav">
<div class="sidenav__section sidenav__section sidenav_action-menu">
<adf-sidebar-action-menu [expanded]="showLabel" [attr.title]="'APP.NEW_MENU.TOOLTIP' | translate" title="{{'APP.NEW_MENU.LABEL' | translate }}">
<mat-icon sidebar-menu-title-icon >arrow_drop_down</mat-icon>
<adf-sidebar-action-menu
[expanded]="showLabel"
[attr.title]="'APP.NEW_MENU.TOOLTIP' | translate"
[title]="'APP.NEW_MENU.LABEL' | translate">
<mat-icon sidebar-menu-title-icon>arrow_drop_down</mat-icon>
<div sidebar-menu-expand-icon>
<mat-icon [title]="'APP.NEW_MENU.TOOLTIP' | translate">queue</mat-icon>
</div>
<div sidebar-menu-options>
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of createActions"
mat-menu-item
[disabled]="entry.disabled"
(click)="runAction(entry.actions.click)">
<mat-icon>{{ entry.icon }}</mat-icon>
<span>{{ entry.title | translate }}</span>
</button>
<ng-container *ngFor="let action of createActions; trackBy: trackById">
<app-toolbar-button
type="menu-item"
[actionRef]="action"></app-toolbar-button>
</ng-container>
<button
mat-menu-item
[disabled]="!canCreateContent"
(click)="createNewFolder()"
[attr.title]="
( canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER'
: 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED'
) | translate">
<mat-icon>create_new_folder</mat-icon>
<span>{{ 'APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER' | translate }}</span>
</button>
<adf-upload-button
[tooltip]="
(canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED'
) | translate"
[disabled]="!canCreateContent"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="false"
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE' | translate">
</adf-upload-button>
<adf-upload-button
[tooltip]="
(canCreateContent
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED'
) | translate"
[disabled]="!canCreateContent"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="true"
[staticTitle]="'APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER' | translate">
</adf-upload-button>
</div>
</adf-sidebar-action-menu>
</div>
<div class="sidenav__section sidenav__section--menu" *ngFor="let group of groups">
<div *ngFor="let group of groups; trackBy: trackById"
class="sidenav__section sidenav__section--menu" >
<ul class="sidenav-menu">
<li *ngFor="let item of group.items" class="sidenav-menu__item"
<li *ngFor="let item of group.items; trackBy: trackById"
class="sidenav-menu__item"
routerLinkActive
#rla="routerLinkActive"
title="{{ item.description | translate }}">
[attr.title]="item.description | translate">
<button
[id]="item.id"
mat-icon-button
mat-ripple
[routerLink]="item.url"
[color]="rla.isActive ? 'accent': 'primary'"
[attr.aria-label]="item.title | translate"
mat-icon-button
mat-ripple
matRippleColor="primary"
[matRippleTrigger]="rippleTrigger"
[matRippleCentered]="true">

View File

@ -25,12 +25,9 @@
import { Subject } from 'rxjs/Rx';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Node } from 'alfresco-js-api';
import { NodePermissionService } from '../../services/node-permission.service';
import { ExtensionService } from '../../extensions/extension.service';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states';
import { CreateFolderAction } from '../../store/actions';
import { currentFolder } from '../../store/selectors/app.selectors';
import { takeUntil } from 'rxjs/operators';
import { NavBarGroupRef } from '../../extensions/navbar.extensions';
@ -44,28 +41,23 @@ import { ContentActionRef } from '../../extensions/action.extensions';
export class SidenavComponent implements OnInit, OnDestroy {
@Input() showLabel: boolean;
node: Node = null;
groups: Array<NavBarGroupRef> = [];
createActions: Array<ContentActionRef> = [];
canCreateContent = false;
onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor(
private store: Store<AppStore>,
private permission: NodePermissionService,
private extensions: ExtensionService
) {
}
) {}
ngOnInit() {
this.groups = this.extensions.getNavigationGroups();
this.store.select(currentFolder)
this.store
.select(currentFolder)
.pipe(takeUntil(this.onDestroy$))
.subscribe(node => {
this.node = node;
.subscribe(() => {
this.createActions = this.extensions.getCreateActions();
this.canCreateContent = node && this.permission.check(node, ['create']);
});
}
@ -74,15 +66,7 @@ export class SidenavComponent implements OnInit, OnDestroy {
this.onDestroy$.complete();
}
createNewFolder() {
if (this.node && this.node.id) {
this.store.dispatch(new CreateFolderAction(this.node.id));
}
}
// this is where each application decides how to treat an action and what to do
// the ACA maps actions to the NgRx actions as an example
runAction(actionId: string) {
this.extensions.runActionById(actionId);
trackById(index: number, obj: { id: string }) {
return obj.id;
}
}

View File

@ -0,0 +1,56 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { documentDisplayMode } from '../../../store/selectors/app.selectors';
import { ToggleDocumentDisplayMode } from '../../../store/actions';
@Component({
selector: 'app-document-display-mode',
template: `
<button
mat-icon-button
color="primary"
(click)="onClick()">
<mat-icon *ngIf="(displayMode$ | async) === 'list'">view_comfy</mat-icon>
<mat-icon *ngIf="(displayMode$ | async) === 'gallery'">list</mat-icon>
</button>
`
})
export class DocumentDisplayModeComponent {
displayMode$: Observable<string>;
constructor(private store: Store<AppStore>) {
this.displayMode$ = store.select(documentDisplayMode);
}
onClick() {
this.store.dispatch(new ToggleDocumentDisplayMode());
}
}

View File

@ -23,25 +23,30 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, Input, HostListener } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states';
import { EditFolderAction } from '../store/actions';
import { AppStore, SelectionState } from '../../../store/states';
import { appSelection } from '../../../store/selectors/app.selectors';
import { Observable } from 'rxjs/Observable';
@Directive({
selector: '[acaEditFolder]'
@Component({
selector: 'app-toggle-favorite',
template: `
<button
mat-menu-item
#favorites="adfFavorite"
[adf-node-favorite]="(selection$ | async).nodes">
<mat-icon *ngIf="favorites.hasFavorites()">star</mat-icon>
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
</button>
`
})
export class EditFolderDirective {
/** Folder node to edit. */
// tslint:disable-next-line:no-input-rename
@Input('acaEditFolder') folder: MinimalNodeEntity;
export class ToggleFavoriteComponent {
@HostListener('click', ['$event'])
onClick(event) {
event.preventDefault();
this.store.dispatch(new EditFolderAction(this.folder));
selection$: Observable<SelectionState>;
constructor(private store: Store<AppStore>) {
this.selection$ = this.store.select(appSelection);
}
constructor(private store: Store<AppStore>) {}
}

View File

@ -0,0 +1,55 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { infoDrawerOpened } from '../../../store/selectors/app.selectors';
import { ToggleInfoDrawerAction } from '../../../store/actions';
@Component({
selector: 'app-toggle-info-drawer',
template: `
<button
mat-icon-button
[color]="(infoDrawerOpened$ | async) ? 'accent' : 'primary'"
[attr.title]="'APP.ACTIONS.DETAILS' | translate"
(click)="onClick()">
<mat-icon>info_outline</mat-icon>
</button>
`
})
export class ToggleInfoDrawerComponent {
infoDrawerOpened$: Observable<boolean>;
constructor(private store: Store<AppStore>) {
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
}
onClick() {
this.store.dispatch(new ToggleInfoDrawerAction());
}
}

View File

@ -4,36 +4,10 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button *ifExperimental="'cardview'"
color="primary"
mat-icon-button
(click)="toggleGalleryView()">
<mat-icon *ngIf="displayMode === 'list'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.CARDVIEW' | translate }}">view_comfy</mat-icon>
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button>
<app-document-display-mode *ifExperimental="'cardview'"></app-document-display-mode>
<ng-container *ifExperimental="'extensions'">
<ng-container *ngFor="let entry of actions">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</ng-container>
<ng-container *ngIf="!selection.isEmpty">
<button
color="primary"
mat-icon-button
[acaPermanentDelete]="selection.nodes"
title="{{ 'APP.ACTIONS.DELETE_PERMANENT' | translate }}">
<mat-icon>delete_forever</mat-icon>
</button>
<button
color="primary"
mat-icon-button
[acaRestoreNode]="selection.nodes"
title="{{ 'APP.ACTIONS.RESTORE' | translate }}">
<mat-icon>restore</mat-icon>
</button>
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [entry]="entry"></aca-toolbar-action>
</ng-container>
</adf-toolbar>
</div>
@ -41,6 +15,7 @@
<div class="inner-layout__content">
<div class="inner-layout__panel">
<adf-document-list acaDocumentList #documentList
[display]="documentDisplayMode$ | async"
currentFolderId="-trashcan-"
selectionMode="multiple"
[navigate]="false"

View File

@ -26,44 +26,17 @@
import { NgModule } from '@angular/core';
import { ExperimentalDirective } from './experimental.directive';
import { DocumentListDirective } from './document-list.directive';
import { EditFolderDirective } from './edit-folder.directive';
import { NodeCopyDirective } from './node-copy.directive';
import { NodeDeleteDirective } from './node-delete.directive';
import { NodeMoveDirective } from './node-move.directive';
import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
import { NodePermissionsDirective } from './node-permissions.directive';
import { NodeRestoreDirective } from './node-restore.directive';
import { NodeUnshareDirective } from './node-unshare.directive';
import { NodeVersionsDirective } from './node-versions.directive';
import { PaginationDirective } from './pagination.directive';
@NgModule({
declarations: [
ExperimentalDirective,
DocumentListDirective,
EditFolderDirective,
NodeCopyDirective,
NodeDeleteDirective,
NodeMoveDirective,
NodePermanentDeleteDirective,
NodePermissionsDirective,
NodeRestoreDirective,
NodeUnshareDirective,
NodeVersionsDirective,
PaginationDirective
],
exports: [
ExperimentalDirective,
DocumentListDirective,
EditFolderDirective,
NodeCopyDirective,
NodeDeleteDirective,
NodeMoveDirective,
NodePermanentDeleteDirective,
NodePermissionsDirective,
NodeRestoreDirective,
NodeUnshareDirective,
NodeVersionsDirective,
PaginationDirective
]
})

View File

@ -38,6 +38,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
})
export class DocumentListDirective implements OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
private isLibrary = false;
get sortingPreferenceKey(): string {
return this.route.snapshot.data.sortingPreferenceKey;
@ -53,6 +54,7 @@ export class DocumentListDirective implements OnInit, OnDestroy {
ngOnInit() {
this.documentList.includeFields = ['isFavorite', 'aspectNames'];
this.documentList.allowDropFiles = false;
this.isLibrary = this.documentList.currentFolderId === '-mysites-';
if (this.sortingPreferenceKey) {
const current = this.documentList.sorting;
@ -103,22 +105,27 @@ export class DocumentListDirective implements OnInit, OnDestroy {
this.unSelectLockedNodes(this.documentList);
}
this.store.dispatch(
new SetSelectedNodesAction(this.documentList.selection)
);
this.updateSelection();
}
}
@HostListener('node-unselect')
onNodeUnselect() {
this.store.dispatch(
new SetSelectedNodesAction(this.documentList.selection)
);
this.updateSelection();
}
onReady() {
this.updateSelection();
}
private updateSelection() {
const selection = this.documentList.selection.map(entry => {
entry['isLibrary'] = this.isLibrary;
return entry;
});
this.store.dispatch(
new SetSelectedNodesAction(this.documentList.selection)
new SetSelectedNodesAction(selection)
);
}

View File

@ -1,307 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { MatSnackBar } from '@angular/material';
import { NodeActionsService } from '../services/node-actions.service';
import { NodeCopyDirective } from './node-copy.directive';
import { ContentApiService } from '../services/content-api.service';
import { AppTestingModule } from '../testing/app-testing.module';
@Component({
template: '<div [acaCopyNode]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeCopyDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let element: DebugElement;
let snackBar: MatSnackBar;
let service: NodeActionsService;
let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ AppTestingModule ],
declarations: [
TestComponent,
NodeCopyDirective
]
});
contentApi = TestBed.get(ContentApiService);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeCopyDirective));
snackBar = TestBed.get(MatSnackBar);
service = TestBed.get(NodeActionsService);
});
describe('Copy node action', () => {
beforeEach(() => {
spyOn(snackBar, 'open').and.callThrough();
});
it('notifies successful copy of a node', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
it('notifies successful copy of multiple nodes', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
const createdItems = [
{ entry: { id: 'copy-of-node-1', name: 'name1' } },
{ entry: { id: 'copy-of-node-2', name: 'name2' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
});
it('notifies partially copy of one node out of a multiple selection of nodes', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
const createdItems = [
{ entry: { id: 'copy-of-node-1', name: 'name1' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR');
});
it('notifies partially copy of more nodes out of a multiple selection of nodes', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [
{ entry: { id: 'node-to-copy-0', name: 'name0' } },
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
const createdItems = [
{ entry: { id: 'copy-of-node-0', name: 'name0' } },
{ entry: { id: 'copy-of-node-1', name: 'name1' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL');
});
it('notifies of failed copy of multiple nodes', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [
{ entry: { id: 'node-to-copy-0', name: 'name0' } },
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
{ entry: { id: 'node-to-copy-2', name: 'name2' } }];
const createdItems = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL');
});
it('notifies of failed copy of one node', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
component.selection = [
{ entry: { id: 'node-to-copy', name: 'name' } }];
const createdItems = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR');
});
it('notifies error if success message was not emitted', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of(''));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next();
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
});
it('notifies permission error on copy of node', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
component.selection = [{ entry: { id: '1', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
});
it('notifies generic error message on all errors, but 403', () => {
spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
component.selection = [{ entry: { id: '1', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
});
});
describe('Undo Copy action', () => {
beforeEach(() => {
spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY'));
spyOn(snackBar, 'open').and.returnValue({
onAction: () => Observable.of({})
});
});
it('should delete the newly created node on Undo action', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(contentApi.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true });
});
it('should delete also the node created inside an already existing folder from destination', () => {
const spyOnDeleteNode = spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
component.selection = [
{ entry: { id: 'node-to-copy-1', name: 'name1' } },
{ entry: { id: 'node-to-copy-2', name: 'folder-with-name-already-existing-on-destination' } }];
const id1 = 'copy-of-node-1';
const id2 = 'copy-of-child-of-node-2';
const createdItems = [
{ entry: { id: id1, name: 'name1' } },
[ { entry: { id: id2, name: 'name-of-child-of-node-2' , parentId: 'the-folder-already-on-destination' } }] ];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
expect(spyOnDeleteNode).toHaveBeenCalled();
expect(spyOnDeleteNode.calls.allArgs())
.toEqual([[id1, { permanent: true }], [id2, { permanent: true }]]);
});
it('notifies when error occurs on Undo action', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
it('notifies when some error of type Error occurs on Undo action', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error('oops!')));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
it('notifies permission error when it occurs on Undo action', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentCopied.next(<any>createdItems);
expect(service.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
});
});

View File

@ -1,155 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { MatSnackBar } from '@angular/material';
import { TranslationService } from '@alfresco/adf-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { NodeActionsService } from '../services/node-actions.service';
import { ContentManagementService } from '../services/content-management.service';
import { ContentApiService } from '../services/content-api.service';
@Directive({
selector: '[acaCopyNode]'
})
export class NodeCopyDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaCopyNode')
selection: MinimalNodeEntity[];
@HostListener('click')
onClick() {
this.copySelected();
}
constructor(
private content: ContentManagementService,
private contentApi: ContentApiService,
private snackBar: MatSnackBar,
private nodeActionsService: NodeActionsService,
private translation: TranslationService
) {}
copySelected() {
Observable.zip(
this.nodeActionsService.copyNodes(this.selection),
this.nodeActionsService.contentCopied
).subscribe(
(result) => {
const [ operationResult, newItems ] = result;
this.toastMessage(operationResult, newItems);
},
(error) => {
this.toastMessage(error);
}
);
}
private toastMessage(info: any, newItems?: MinimalNodeEntity[]) {
const numberOfCopiedItems = newItems ? newItems.length : 0;
const failedItems = this.selection.length - numberOfCopiedItems;
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
if (typeof info === 'string') {
if (info.toLowerCase().indexOf('succes') !== -1) {
let i18MessageSuffix;
if (failedItems) {
if (numberOfCopiedItems) {
i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'PARTIAL_SINGULAR' : 'PARTIAL_PLURAL';
} else {
i18MessageSuffix = ( failedItems === 1 ) ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
}
} else {
i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'SINGULAR' : 'PLURAL';
}
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
}
} else {
try {
const { error: { statusCode } } = JSON.parse(info.message);
if (statusCode === 403) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
} catch (err) { /* Do nothing, keep the original message */ }
}
const undo = (numberOfCopiedItems > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
const message = this.translation.instant(i18nMessageString, { success: numberOfCopiedItems, failed: failedItems });
this.snackBar
.open(message, undo, {
panelClass: 'info-snackbar',
duration: 3000
})
.onAction()
.subscribe(() => this.deleteCopy(newItems));
}
private deleteCopy(nodes: MinimalNodeEntity[]) {
const batch = this.nodeActionsService.flatten(nodes)
.filter(item => item.entry)
.map(item => this.contentApi.deleteNode(item.entry.id, { permanent: true }));
Observable.forkJoin(...batch)
.subscribe(
() => {
this.content.nodesDeleted.next(null);
},
(error) => {
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
let errorJson = null;
try {
errorJson = JSON.parse(error.message);
} catch (e) { //
}
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
const message = this.translation.instant(i18nMessageString);
this.snackBar.open(message, '', {
panelClass: 'error-snackbar',
duration: 3000
});
}
);
}
}

View File

@ -1,274 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement } from '@angular/core';
import { NodeDeleteDirective } from './node-delete.directive';
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';
import { AppTestingModule } from '../testing/app-testing.module';
import { ContentApiService } from '../services/content-api.service';
import { Observable } from 'rxjs/Rx';
@Component({
template: '<div [acaDeleteNode]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeDeleteDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let actions$: Actions;
let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects])
],
declarations: [
NodeDeleteDirective,
TestComponent
]
});
contentApi = TestBed.get(ContentApiService);
actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeDeleteDirective));
});
describe('Delete action', () => {
it('should raise info message on successful single file deletion', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map(action => {
done();
})
);
component.selection = [{ entry: { id: '1', name: 'name1' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise error message on failed single file deletion', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => {
done();
})
);
component.selection = [{ entry: { id: '1', name: 'name1' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise info message on successful multiple files deletion', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null));
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map(action => {
done();
})
);
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise error message failed multiple files deletion', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null));
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => {
done();
})
);
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise warning message when only one file is successful', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
if (id === '1') {
return Observable.throw(null);
} else {
return Observable.of(null);
}
});
actions$.pipe(
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
map(action => {
done();
})
);
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise warning message when some files are successfully deleted', fakeAsync(done => {
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
if (id === '1') {
return Observable.throw(null);
}
if (id === '2') {
return Observable.of(null);
}
if (id === '3') {
return Observable.of(null);
}
});
actions$.pipe(
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
map(action => {
done();
})
);
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
});
/*
describe('Restore action', () => {
beforeEach(() => {
spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null));
});
it('notifies failed file on on restore', () => {
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null));
component.selection = [
{ entry: { id: '1', name: 'name1' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(spySnackBar.calls.mostRecent().args)
.toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE', '', 3000]));
});
it('notifies failed files on on restore', () => {
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null));
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(spySnackBar.calls.mostRecent().args)
.toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', '', 3000]));
});
it('signals files restored', () => {
spyOn(contentService.nodeRestored, 'next');
spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.callFake((id) => {
if (id === '1') {
return Promise.resolve(null);
} else {
return Promise.reject(null);
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentService.nodeRestored.next).toHaveBeenCalled();
});
});
*/
});

View File

@ -1,59 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state';
import { DeleteNodesAction } from '../store/actions';
import { NodeInfo } from '../store/models';
@Directive({
selector: '[acaDeleteNode]'
})
export class NodeDeleteDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaDeleteNode')
selection: MinimalNodeEntity[];
constructor(private store: Store<AppStore>) {}
@HostListener('click')
onClick() {
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;
return {
id,
name
};
});
this.store.dispatch(new DeleteNodesAction(toDelete));
}
}
}

View File

@ -1,477 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { MatSnackBar } from '@angular/material';
import { TranslationService } from '@alfresco/adf-core';
import { NodeActionsService } from '../services/node-actions.service';
import { NodeMoveDirective } from './node-move.directive';
import { EffectsModule, Actions, ofType } from '@ngrx/effects';
import { NodeEffects } from '../store/effects/node.effects';
import { SnackbarErrorAction, SNACKBAR_ERROR } from '../store/actions';
import { map } from 'rxjs/operators';
import { AppTestingModule } from '../testing/app-testing.module';
import { ContentApiService } from '../services/content-api.service';
@Component({
template: '<div [acaMoveNode]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeMoveDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let element: DebugElement;
let service: NodeActionsService;
let actions$: Actions;
let translationService: TranslationService;
let contentApi: ContentApiService;
let snackBar: MatSnackBar;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects])
],
declarations: [
NodeMoveDirective,
TestComponent
]
});
contentApi = TestBed.get(ContentApiService);
translationService = TestBed.get(TranslationService);
actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeMoveDirective));
service = TestBed.get(NodeActionsService);
snackBar = TestBed.get(MatSnackBar);
});
beforeEach(() => {
spyOn(translationService, 'instant').and.callFake((keysArray) => {
if (Array.isArray(keysArray)) {
const processedKeys = {};
keysArray.forEach((key) => {
processedKeys[key] = key;
});
return processedKeys;
} else {
return keysArray;
}
});
});
describe('Move node action', () => {
beforeEach(() => {
spyOn(snackBar, 'open').and.callThrough();
});
it('notifies successful move of a node', () => {
const node = [ { entry: { id: 'node-to-move-id', name: 'name' } } ];
const moveResponse = {
succeeded: node,
failed: [],
partiallySucceeded: []
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = node;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('notifies successful move of multiple nodes', () => {
const nodes = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }];
const moveResponse = {
succeeded: nodes,
failed: [],
partiallySucceeded: []
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = nodes;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL');
});
it('notifies partial move of a node', () => {
const node = [ { entry: { id: '1', name: 'name' } } ];
const moveResponse = {
succeeded: [],
failed: [],
partiallySucceeded: node
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = node;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
});
it('notifies partial move of multiple nodes', () => {
const nodes = [
{ entry: { id: '1', name: 'name' } },
{ entry: { id: '2', name: 'name2' } } ];
const moveResponse = {
succeeded: [],
failed: [],
partiallySucceeded: nodes
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = nodes;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL');
});
it('notifies successful move and the number of nodes that could not be moved', () => {
const nodes = [ { entry: { id: '1', name: 'name' } },
{ entry: { id: '2', name: 'name2' } } ];
const moveResponse = {
succeeded: [ nodes[0] ],
failed: [ nodes[1] ],
partiallySucceeded: []
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = nodes;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0])
.toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL');
});
it('notifies successful move and the number of partially moved ones', () => {
const nodes = [ { entry: { id: '1', name: 'name' } },
{ entry: { id: '2', name: 'name2' } } ];
const moveResponse = {
succeeded: [ nodes[0] ],
failed: [],
partiallySucceeded: [ nodes[1] ]
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = nodes;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0])
.toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
});
it('notifies error if success message was not emitted', () => {
const node = { entry: { id: 'node-to-move-id', name: 'name' } };
const moveResponse = {
succeeded: [],
failed: [],
partiallySucceeded: []
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of(''));
component.selection = [ node ];
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
});
it('notifies permission error on move of node', () => {
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
component.selection = [{ entry: { id: '1', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
});
it('notifies generic error message on all errors, but 403', () => {
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}}))));
component.selection = [{ entry: { id: '1', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
});
it('notifies conflict error message on 409', () => {
spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}}))));
component.selection = [{ entry: { id: '1', name: 'name' } }];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE');
});
it('notifies error if move response has only failed items', () => {
const node = [ { entry: { id: '1', name: 'name' } } ];
const moveResponse = {
succeeded: [],
failed: [ {} ],
partiallySucceeded: []
};
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(service, 'processResponse').and.returnValue(moveResponse);
component.selection = node;
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(moveResponse);
expect(service.moveNodes).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
});
});
describe('Undo Move action', () => {
beforeEach(() => {
spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE'));
spyOn(snackBar, 'open').and.returnValue({
onAction: () => Observable.of({})
});
// spyOn(snackBar, 'open').and.callThrough();
});
it('should move node back to initial parent, after succeeded move', () => {
const initialParent = 'parent-id-0';
const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } };
component.selection = [ node ];
spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({}));
fixture.detectChanges();
element.triggerEventHandler('click', null);
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [ { itemMoved: node, initialParentId: initialParent} ]
};
service.contentMoved.next(<any>movedItems);
expect(service.moveNodeAction)
.toHaveBeenCalledWith(movedItems.succeeded[0].itemMoved.entry, movedItems.succeeded[0].initialParentId);
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should move node back to initial parent, after succeeded move of a single file', () => {
const initialParent = 'parent-id-0';
const node = { entry: { id: 'node-to-move-id', name: 'name', isFolder: false, parentId: initialParent } };
component.selection = [ node ];
spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({}));
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [ node ]
};
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(<any>movedItems);
expect(service.moveNodeAction).toHaveBeenCalledWith(node.entry, initialParent);
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should restore deleted folder back to initial parent, after succeeded moving all its files', () => {
// when folder was deleted after all its children were moved to a folder with the same name from destination
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of(null));
const initialParent = 'parent-id-0';
const node = { entry: { id: 'folder-to-move-id', name: 'conflicting-name', parentId: initialParent, isFolder: true } };
component.selection = [ node ];
const itemMoved = {}; // folder was empty
service.moveDeletedEntries = [ node ]; // folder got deleted
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [ [ itemMoved ] ]
};
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(<any>movedItems);
expect(contentApi.restoreNode).toHaveBeenCalled();
expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should notify when error occurs on Undo Move action', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(null));
actions$.pipe(
ofType<SnackbarErrorAction>(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];
const afterMoveParentId = 'parent-id-1';
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name', parentId: afterMoveParentId } };
service.moveDeletedEntries = [ node ]; // folder got deleted
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
};
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(<any>movedItems);
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error('oops!')));
actions$.pipe(
ofType<SnackbarErrorAction>(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 ];
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } };
service.moveDeletedEntries = [ node ]; // folder got deleted
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
};
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(<any>movedItems);
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
it('should notify permission error when it occurs on Undo Move action', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}}))));
actions$.pipe(
ofType<SnackbarErrorAction>(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 ];
const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } };
service.moveDeletedEntries = [ node ]; // folder got deleted
const movedItems = {
failed: [],
partiallySucceeded: [],
succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }]
};
fixture.detectChanges();
element.triggerEventHandler('click', null);
service.contentMoved.next(<any>movedItems);
expect(service.moveNodes).toHaveBeenCalled();
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
});
});

View File

@ -1,223 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { TranslationService } from '@alfresco/adf-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { MatSnackBar } from '@angular/material';
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';
import { ContentApiService } from '../services/content-api.service';
@Directive({
selector: '[acaMoveNode]'
})
export class NodeMoveDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaMoveNode')
selection: MinimalNodeEntity[];
@HostListener('click')
onClick() {
this.moveSelected();
}
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private content: ContentManagementService,
private nodeActionsService: NodeActionsService,
private translation: TranslationService,
private snackBar: MatSnackBar
) {}
moveSelected() {
const permissionForMove = '!';
Observable.zip(
this.nodeActionsService.moveNodes(this.selection, permissionForMove),
this.nodeActionsService.contentMoved
).subscribe(
(result) => {
const [ operationResult, moveResponse ] = result;
this.toastMessage(operationResult, moveResponse);
this.content.nodesMoved.next(null);
},
(error) => {
this.toastMessage(error);
}
);
}
private toastMessage(info: any, moveResponse?: any) {
const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0;
const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0;
const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0;
let successMessage = '';
let partialSuccessMessage = '';
let failedMessage = '';
let errorMessage = '';
if (typeof info === 'string') {
// in case of success
if (info.toLowerCase().indexOf('succes') !== -1) {
const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.';
let i18MessageSuffix = '';
if (succeeded) {
i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL';
successMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
if (partiallySucceeded) {
i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL';
partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
if (failures) {
// if moving failed for ALL nodes, emit error
if (failures === this.selection.length) {
const errors = this.nodeActionsService.flatten(moveResponse['failed']);
errorMessage = this.getErrorMessage(errors[0]);
} else {
i18MessageSuffix = 'PARTIAL.FAIL';
failedMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
}
} else {
errorMessage = 'APP.MESSAGES.ERRORS.GENERIC';
}
} else {
errorMessage = this.getErrorMessage(info);
}
const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
failedMessage = errorMessage ? errorMessage : failedMessage;
const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : '';
const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : '';
const initialParentId = this.nodeActionsService.getEntryParentId(this.selection[0].entry);
const messages = this.translation.instant(
[successMessage, partialSuccessMessage, failedMessage],
{ success: succeeded, failed: failures, partially: partiallySucceeded}
);
// TODO: review in terms of i18n
this.snackBar
.open(
messages[successMessage]
+ beforePartialSuccessMessage + messages[partialSuccessMessage]
+ beforeFailedMessage + messages[failedMessage]
, undo, {
panelClass: 'info-snackbar',
duration: 3000
})
.onAction()
.subscribe(() => this.revertMoving(moveResponse, initialParentId));
}
getErrorMessage(errorObject): string {
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
try {
const { error: { statusCode } } = JSON.parse(errorObject.message);
if (statusCode === 409) {
i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE';
} else if (statusCode === 403) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
} catch (err) { /* Do nothing, keep the original message */ }
return i18nMessageString;
}
private revertMoving(moveResponse, selectionParentId) {
const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : [];
const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : [];
const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries
.map((folderEntry) => {
return this.contentApi
.restoreNode(folderEntry.nodeId || folderEntry.id)
.map(node => node.entry);
});
Observable.zip(...restoreDeletedNodesBatch, Observable.of(null))
.flatMap(() => {
const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes];
const revertMoveBatch = this.nodeActionsService
.flatten(nodesToBeMovedBack)
.filter(node => node.entry || (node.itemMoved && node.itemMoved.entry))
.map((node) => {
if (node.itemMoved) {
return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId);
} else {
return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId);
}
});
return Observable.zip(...revertMoveBatch, Observable.of(null));
})
.subscribe(
() => {
this.content.nodesMoved.next(null);
},
error => {
let message = 'APP.MESSAGES.ERRORS.GENERIC';
let errorJson = null;
try {
errorJson = JSON.parse(error.message);
} catch {}
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
message = 'APP.MESSAGES.ERRORS.PERMISSION';
}
this.store.dispatch(new SnackbarErrorAction(message));
}
);
}
}

View File

@ -1,272 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component, DebugElement } from '@angular/core';
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
import { MatDialog } from '@angular/material';
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 { AppTestingModule } from '../testing/app-testing.module';
import { ContentApiService } from '../services/content-api.service';
@Component({
template: `<div [acaPermanentDelete]="selection"></div>`
})
class TestComponent {
selection = [];
}
describe('NodePermanentDeleteDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let component: TestComponent;
let dialog: MatDialog;
let actions$: Actions;
let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects])
],
declarations: [
NodePermanentDeleteDirective,
TestComponent
]
});
contentApi = TestBed.get(ContentApiService);
actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective));
dialog = TestBed.get(MatDialog);
spyOn(dialog, 'open').and.returnValue({
afterClosed() {
return Observable.of(true);
}
});
});
it('does not purge nodes if no selection', () => {
spyOn(contentApi, 'purgeDeletedNode');
component.selection = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentApi.purgeDeletedNode).not.toHaveBeenCalled();
});
it('call purge nodes if selection is not empty', fakeAsync(() => {
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({}));
component.selection = [ { entry: { id: '1' } } ];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
expect(contentApi.purgeDeletedNode).toHaveBeenCalled();
}));
describe('notification', () => {
it('raises warning on multiple fail and one success', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
map((action: SnackbarWarningAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return Observable.of({});
}
if (id === '2') {
return Observable.throw({});
}
if (id === '3') {
return Observable.throw({});
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('raises warning on multiple success and multiple fail', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarWarningAction>(SNACKBAR_WARNING),
map((action: SnackbarWarningAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return Observable.of({});
}
if (id === '2') {
return Observable.throw({});
}
if (id === '3') {
return Observable.throw({});
}
if (id === '4') {
return Observable.of({});
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } },
{ entry: { id: '3', name: 'name3' } },
{ entry: { id: '4', name: 'name4' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('raises info on one selected node success', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map((action: SnackbarInfoAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({}));
component.selection = [
{ entry: { id: '1', name: 'name1' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('raises error on one selected node fail', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map((action: SnackbarErrorAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.throw({}));
component.selection = [
{ entry: { id: '1', name: 'name1' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('raises info on all nodes success', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map((action: SnackbarInfoAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return Observable.of({});
}
if (id === '2') {
return Observable.of({});
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('raises error on all nodes fail', fakeAsync(done => {
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map((action: SnackbarErrorAction) => {
done();
})
);
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return Observable.throw({});
}
if (id === '2') {
return Observable.throw({});
}
});
component.selection = [
{ entry: { id: '1', name: 'name1' } },
{ entry: { id: '2', name: 'name2' } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
});
});

View File

@ -1,80 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { MatDialog } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state';
import { SnackbarErrorAction } from '../store/actions';
import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog';
@Directive({
selector: '[acaNodePermissions]'
})
export class NodePermissionsDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaNodePermissions') node: MinimalNodeEntity;
@HostListener('click')
onClick() {
this.showPermissions();
}
constructor(
private store: Store<AppStore>,
private dialog: MatDialog
) {}
showPermissions() {
if (this.node) {
let entry;
if (this.node.entry) {
entry = this.node.entry;
} else {
entry = this.node;
}
const entryId = entry.nodeId || (<any>entry).guid || entry.id;
this.openPermissionsDialog(entryId);
}
}
openPermissionsDialog(nodeId: string) {
// workaround Shared
if (nodeId) {
this.dialog.open(NodePermissionsDialogComponent, {
data: { nodeId },
panelClass: 'aca-permissions-dialog-panel',
width: '730px'
});
} else {
this.store.dispatch(
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
);
}
}
}

View File

@ -1,390 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component, DebugElement } from '@angular/core';
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NodeRestoreDirective } from './node-restore.directive';
import { ContentManagementService } from '../services/content-management.service';
import { Actions, ofType, EffectsModule } from '@ngrx/effects';
import { SnackbarErrorAction,
SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO,
NavigateRouteAction, NAVIGATE_ROUTE } from '../store/actions';
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: Array<MinimalNodeEntity> = [];
}
describe('NodeRestoreDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let component: TestComponent;
let contentManagementService: ContentManagementService;
let actions$: Actions;
let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects])
],
declarations: [
NodeRestoreDirective,
TestComponent
]
});
actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeRestoreDirective));
contentManagementService = TestBed.get(ContentManagementService);
contentApi = TestBed.get(ContentApiService);
});
it('does not restore nodes if no selection', () => {
spyOn(contentApi, 'restoreNode');
component.selection = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentApi.restoreNode).not.toHaveBeenCalled();
});
it('does not restore nodes if selection has nodes without path', () => {
spyOn(contentApi, 'restoreNode');
component.selection = [ { entry: { id: '1' } } ];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentApi.restoreNode).not.toHaveBeenCalled();
});
it('call restore nodes if selection has nodes with path', fakeAsync(() => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
list: { entries: [] }
}));
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
path
}
}
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
describe('refresh()', () => {
it('dispatch event on finish', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
list: { entries: [] }
}));
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
path
}
}
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
contentManagementService.nodesRestored.subscribe(() => done());
}));
});
describe('notification', () => {
beforeEach(() => {
spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({
list: { entries: [] }
}));
});
it('should raise error message on partial multiple fail ', fakeAsync(done => {
const error = { message: '{ "error": {} }' };
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => done())
);
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
if (id === '1') {
return Observable.of({});
}
if (id === '2') {
return Observable.throw(error);
}
if (id === '3') {
return Observable.throw(error);
}
});
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } },
{ entry: { id: '2', name: 'name2', path } },
{ entry: { id: '3', name: 'name3', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise error message when restored node exist, error 409', fakeAsync(done => {
const error = { message: '{ "error": { "statusCode": 409 } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise error message when restored node returns different statusCode', fakeAsync(done => {
const error = { message: '{ "error": { "statusCode": 404 } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise error message when restored node location is missing', fakeAsync(done => {
const error = { message: '{ "error": { } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error));
actions$.pipe(
ofType<SnackbarErrorAction>(SNACKBAR_ERROR),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('should raise info message when restore multiple nodes', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
if (id === '1') {
return Observable.of({});
}
if (id === '2') {
return Observable.of({});
}
});
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } },
{ entry: { id: '2', name: 'name2', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
xit('should raise info message when restore selected node', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
actions$.pipe(
ofType<SnackbarInfoAction>(SNACKBAR_INFO),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{ entry: { id: '1', name: 'name1', path } }
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
it('navigate to restore selected node location onAction', fakeAsync(done => {
spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({}));
actions$.pipe(
ofType<NavigateRouteAction>(NAVIGATE_ROUTE),
map(action => done())
);
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
component.selection = [
{
entry: {
id: '1',
name: 'name1',
path
}
}
];
fixture.detectChanges();
element.triggerEventHandler('click', null);
tick();
}));
});
});

View File

@ -1,57 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { ContentManagementService } from '../services/content-management.service';
import { ContentApiService } from '../services/content-api.service';
@Directive({
selector: '[acaUnshareNode]'
})
export class NodeUnshareDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaUnshareNode')
selection: MinimalNodeEntity[];
constructor(
private contentApi: ContentApiService,
private contentManagement: ContentManagementService) {
}
@HostListener('click')
onClick() {
if (this.selection.length > 0) {
this.unshareLinks(this.selection);
}
}
private async unshareLinks(links: MinimalNodeEntity[]) {
const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise());
await Promise.all(promises);
this.contentManagement.linksUnshared.next();
}
}

View File

@ -1,84 +0,0 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog';
import { MatDialog } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states/app.state';
import { SnackbarErrorAction } from '../store/actions';
import { ContentApiService } from '../services/content-api.service';
@Directive({
selector: '[acaNodeVersions]'
})
export class NodeVersionsDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaNodeVersions') node: MinimalNodeEntity;
@HostListener('click')
onClick() {
this.onManageVersions();
}
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private dialog: MatDialog
) {}
async onManageVersions() {
if (this.node && this.node.entry) {
let entry = this.node.entry;
if (entry.nodeId || (<any>entry).guid) {
entry = await this.contentApi.getNodeInfo(
entry.nodeId || (<any>entry).id
).toPromise();
this.openVersionManagerDialog(entry);
} else {
this.openVersionManagerDialog(entry);
}
} else if (this.node) {
this.openVersionManagerDialog(<MinimalNodeEntryEntity>this.node);
}
}
openVersionManagerDialog(node: MinimalNodeEntryEntity) {
// workaround Shared
if (node.isFile || node.nodeId) {
this.dialog.open(NodeVersionsDialogComponent, {
data: { node },
panelClass: 'adf-version-manager-dialog-panel',
width: '630px'
});
} else {
this.store.dispatch(
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
);
}
}
}

View File

@ -24,7 +24,7 @@
*/
export enum ContentActionType {
default = 'button',
default = 'default',
button = 'button',
separator = 'separator',
menu = 'menu',
@ -36,6 +36,7 @@ export interface ContentActionRef {
type: ContentActionType;
title?: string;
description?: string;
order?: number;
icon?: string;
disabled?: boolean;

View File

@ -1,37 +0,0 @@
<ng-container [ngSwitch]="entry.type">
<button *ngSwitchCase="'button'"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.actions.click)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
<adf-toolbar-divider *ngSwitchCase="'separator'"></adf-toolbar-divider>
<ng-container *ngSwitchCase="'menu'">
<button
color="primary"
mat-icon-button
title="{{ entry.title | translate }}"
[matMenuTriggerFor]="menu">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
<mat-menu #menu="matMenu"
[overlapTrigger]="false">
<ng-container *ngFor="let child of entry.children">
<button mat-menu-item
(click)="runAction(child.actions.click)">
<mat-icon>{{ child.icon }}</mat-icon>
<span>{{ child.title | translate }}</span>
</button>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<app-custom-component [id]="entry.component"></app-custom-component>
</ng-container>
</ng-container>

View File

@ -0,0 +1,39 @@
<ng-container [ngSwitch]="entry.type">
<ng-container *ngSwitchCase="'button'">
<app-toolbar-button [type]="type" [actionRef]="entry"></app-toolbar-button>
</ng-container>
<adf-toolbar-divider *ngSwitchCase="'separator'"
[id]="entry.id">
</adf-toolbar-divider>
<ng-container *ngSwitchCase="'menu'">
<button
[id]="entry.id"
color="primary"
mat-icon-button
[attr.title]="(entry.description || entry.title) | translate"
[matMenuTriggerFor]="menu">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
<mat-menu #menu="matMenu"
[overlapTrigger]="false">
<ng-container *ngFor="let child of entry.children; trackBy: trackByActionId">
<ng-container [ngSwitch]="child.type">
<ng-container *ngSwitchCase="'custom'">
<app-custom-component [id]="child.component"></app-custom-component>
</ng-container>
<ng-container *ngSwitchDefault>
<app-toolbar-button type="menu-item" [actionRef]="child"></app-toolbar-button>
</ng-container>
</ng-container>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<app-custom-component [id]="entry.component"></app-custom-component>
</ng-container>
</ng-container>

View File

@ -27,16 +27,11 @@ import {
Component,
ViewEncapsulation,
ChangeDetectionStrategy,
Input,
OnInit,
OnDestroy
Input
} from '@angular/core';
import { AppStore, SelectionState } from '../../../store/states';
import { AppStore } from '../../../store/states';
import { Store } from '@ngrx/store';
import { ExtensionService } from '../../extension.service';
import { appSelection } from '../../../store/selectors/app.selectors';
import { Subject } from 'rxjs/Rx';
import { takeUntil } from 'rxjs/operators';
import { ContentActionRef } from '../../action.extensions';
@Component({
@ -46,36 +41,16 @@ import { ContentActionRef } from '../../action.extensions';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'aca-toolbar-action' }
})
export class ToolbarActionComponent implements OnInit, OnDestroy {
export class ToolbarActionComponent {
@Input() type = 'icon-button';
@Input() entry: ContentActionRef;
selection: SelectionState;
onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor(
protected store: Store<AppStore>,
protected extensions: ExtensionService
) {}
ngOnInit() {
this.store
.select(appSelection)
.pipe(takeUntil(this.onDestroy$))
.subscribe(selection => {
this.selection = selection;
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
runAction(actionId: string) {
const context = {
selection: this.selection
};
this.extensions.runActionById(actionId, context);
trackByActionId(index: number, action: ContentActionRef) {
return action.id;
}
}

View File

@ -0,0 +1,90 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Component, Input } from '@angular/core';
import { ContentActionRef } from '../../action.extensions';
import { ExtensionService } from '../../extension.service';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { appSelection } from '../../../store/selectors/app.selectors';
export enum ToolbarButtonType {
ICON_BUTTON = 'icon-button',
MENU_ITEM = 'menu-item'
}
@Component({
selector: 'app-toolbar-button',
template: `
<ng-container [ngSwitch]="type">
<ng-container *ngSwitchCase="'icon-button'">
<button
[id]="actionRef.id"
mat-icon-button
color="primary"
[attr.title]="(actionRef.description || actionRef.title) | translate"
(click)="runAction()">
<mat-icon>{{ actionRef.icon }}</mat-icon>
</button>
</ng-container>
<ng-container *ngSwitchCase="'menu-item'">
<button
[id]="actionRef.id"
mat-menu-item
color="primary"
[disabled]="actionRef.disabled"
[attr.title]="(
actionRef.disabled
? actionRef['description-disabled']
: actionRef.description || actionRef.title
) | translate"
(click)="runAction()">
<mat-icon>{{ actionRef.icon }}</mat-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
</ng-container>
</ng-container>
`
})
export class ToolbarButtonComponent {
@Input() type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON;
@Input() actionRef: ContentActionRef;
constructor(
protected store: Store<AppStore>,
private extensions: ExtensionService
) {}
runAction() {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
this.extensions.runActionById(this.actionRef.actions.click, {
selection
});
});
}
}

View File

@ -28,11 +28,14 @@ import { CommonModule } from '@angular/common';
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { LayoutComponent } from '../components/layout/layout.component';
import { TrashcanComponent } from '../components/trashcan/trashcan.component';
import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component';
import { ToolbarActionComponent } from './components/toolbar/toolbar-action.component';
import * as app from './evaluators/app.evaluators';
import * as nav from './evaluators/navigation.evaluators';
import { ExtensionService } from './extension.service';
import { CustomExtensionComponent } from './components/custom-component/custom.component';
import { DemoButtonComponent } from './components/custom-component/demo.button';
import { ToggleInfoDrawerComponent } from '../components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
import { ToggleFavoriteComponent } from '../components/toolbar/toggle-favorite/toggle-favorite.component';
import { ToolbarButtonComponent } from './components/toolbar/toolbar-button.component';
export function setupExtensions(extensions: ExtensionService): Function {
return () =>
@ -40,7 +43,8 @@ export function setupExtensions(extensions: ExtensionService): Function {
extensions.setComponents({
'app.layout.main': LayoutComponent,
'app.components.trashcan': TrashcanComponent,
'app.demo.button': DemoButtonComponent
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
'app.toolbar.toggleFavorite': ToggleFavoriteComponent
});
extensions.setAuthGuards({
@ -48,14 +52,33 @@ export function setupExtensions(extensions: ExtensionService): Function {
});
extensions.setEvaluators({
'app.selection.canDelete': app.canDeleteSelection,
'app.selection.canDownload': app.canDownloadSelection,
'app.selection.notEmpty': app.hasSelection,
'app.selection.canUnshare': app.canUnshareNodes,
'app.selection.canAddFavorite': app.canAddFavorite,
'app.selection.canRemoveFavorite': app.canRemoveFavorite,
'app.selection.first.canUpdate': app.canUpdateSelectedNode,
'app.selection.file': app.hasFileSelected,
'app.selection.file.canShare': app.canShareFile,
'app.selection.library': app.hasLibrarySelected,
'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
'app.navigation.folder.canUpload': app.canUpload,
'app.navigation.isTrashcan': nav.isTrashcan,
'app.navigation.isNotTrashcan': nav.isNotTrashcan,
'app.navigation.isLibraries': nav.isLibraries,
'app.navigation.isNotLibraries': nav.isNotLibraries,
'app.navigation.isSharedFiles': nav.isSharedFiles,
'app.navigation.isNotSharedFiles': nav.isNotSharedFiles,
'app.navigation.isFavorites': nav.isFavorites,
'app.navigation.isNotFavorites': nav.isNotFavorites,
'app.navigation.isRecentFiles': nav.isRecentFiles,
'app.navigation.isNotRecentFiles': nav.isNotRecentFiles,
'app.navigation.isSearchResults': nav.isSearchResults,
'app.navigation.isNotSearchResults': nav.isNotSearchResults
});
resolve(true);
@ -66,15 +89,13 @@ export function setupExtensions(extensions: ExtensionService): Function {
imports: [CommonModule, CoreModule.forChild()],
declarations: [
ToolbarActionComponent,
CustomExtensionComponent,
DemoButtonComponent
ToolbarButtonComponent,
CustomExtensionComponent
],
exports: [
ToolbarActionComponent,
ToolbarButtonComponent,
CustomExtensionComponent
],
entryComponents: [
DemoButtonComponent
]
})
export class CoreExtensionsModule {

View File

@ -23,63 +23,201 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Node } from 'alfresco-js-api';
import { RuleContext, RuleParameter } from '../rule.extensions';
import {
isNotTrashcan,
isNotSharedFiles,
isNotLibraries,
isFavorites,
isLibraries,
isTrashcan,
isSharedFiles,
isNotSearchResults
} from './navigation.evaluators';
export function isTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean {
const { url } = context.navigation;
return url && url.startsWith('/trashcan');
}
export function isNotTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean {
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) {
return nodeHasPermission(folder, 'create');
export function canAddFavorite(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (!context.selection.isEmpty) {
if (
isFavorites(context, ...args) ||
isLibraries(context, ...args) ||
isTrashcan(context, ...args)
) {
return false;
}
return context.selection.nodes.some(node => !node.entry.isFavorite);
}
return false;
}
export function canDownloadSelection(context: RuleContext, ...args: RuleParameter[]): boolean {
export function canRemoveFavorite(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (!context.selection.isEmpty && !isTrashcan(context, ...args)) {
if (isFavorites(context, ...args)) {
return true;
}
return context.selection.nodes.every(node => node.entry.isFavorite);
}
return false;
}
export function canShareFile(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (
isNotTrashcan(context, ...args) &&
isNotSharedFiles(context, ...args) &&
context.selection.file
) {
return true;
}
return false;
}
export function canDeleteSelection(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (
isNotTrashcan(context, ...args) &&
isNotLibraries(context, ...args) &&
isNotSearchResults(context, ...args) &&
!context.selection.isEmpty
) {
// temp workaround for Search api
if (isFavorites(context, ...args)) {
return true;
}
// workaround for Shared Files
if (isSharedFiles(context, ...args)) {
return context.permissions.check(
context.selection.nodes,
['delete'],
{ target: 'allowableOperationsOnTarget' });
}
return context.permissions.check(context.selection.nodes, ['delete']);
}
return false;
}
export function canUnshareNodes(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (!context.selection.isEmpty) {
return context.selection.nodes.every(node => {
return node.entry && (node.entry.isFile || node.entry.isFolder || !!node.entry.nodeId);
return context.permissions.check(context.selection.nodes, ['delete'], {
target: 'allowableOperationsOnTarget'
});
}
return false;
}
export function hasFolderSelected(context: RuleContext, ...args: RuleParameter[]): boolean {
export function hasSelection(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !context.selection.isEmpty;
}
export function canCreateFolder(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { currentFolder } = context.navigation;
if (currentFolder) {
return context.permissions.check(currentFolder, ['create']);
}
return false;
}
export function canUpload(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { currentFolder } = context.navigation;
if (currentFolder) {
return context.permissions.check(currentFolder, ['create']);
}
return false;
}
export function canDownloadSelection(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (!context.selection.isEmpty) {
return context.selection.nodes.every(node => {
return (
node.entry &&
(node.entry.isFile ||
node.entry.isFolder ||
!!node.entry.nodeId)
);
});
}
return false;
}
export function hasFolderSelected(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const folder = context.selection.folder;
return folder ? true : false;
}
export function hasFileSelected(context: RuleContext, ...args: RuleParameter[]): boolean {
export function hasLibrarySelected(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const library = context.selection.library;
return library ? true : false;
}
export function hasFileSelected(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const file = context.selection.file;
return file ? true : false;
}
export function canUpdateSelectedFolder(context: RuleContext, ...args: RuleParameter[]): boolean {
const folder = context.selection.folder;
if (folder && folder.entry) {
return nodeHasPermission(folder.entry, 'update');
export function canUpdateSelectedNode(
context: RuleContext,
...args: RuleParameter[]
): boolean {
if (context.selection && !context.selection.isEmpty) {
const node = context.selection.first;
if (node.entry.hasOwnProperty('allowableOperationsOnTarget')) {
return context.permissions.check(node, ['update'], {
target: 'allowableOperationsOnTarget'
});
}
return context.permissions.check(node, ['update']);
}
return false;
}
export function nodeHasPermission(node: Node, permission: string): boolean {
if (node && permission) {
const allowableOperations = node.allowableOperations || [];
return allowableOperations.includes(permission);
export function canUpdateSelectedFolder(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { folder } = context.selection;
if (folder) {
return (
// workaround for Search Api
isFavorites(context, ...args) ||
context.permissions.check(folder.entry, ['update'])
);
}
return false;
}

View File

@ -0,0 +1,116 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { RuleContext, RuleParameter } from '../rule.extensions';
export function isFavorites(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.startsWith('/favorites');
}
export function isNotFavorites(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isFavorites(context, ...args);
}
export function isSharedFiles(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.startsWith('/shared');
}
export function isNotSharedFiles(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isSharedFiles(context, ...args);
}
export function isTrashcan(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.startsWith('/trashcan');
}
export function isNotTrashcan(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isTrashcan(context, ...args);
}
export function isLibraries(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.endsWith('/libraries');
}
export function isNotLibraries(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isLibraries(context, ...args);
}
export function isRecentFiles(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.startsWith('/recent-files');
}
export function isNotRecentFiles(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isRecentFiles(context, ...args);
}
export function isSearchResults(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.startsWith('/search');
}
export function isNotSearchResults(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !isSearchResults(context, ...args);
}

View File

@ -41,6 +41,7 @@ export interface ExtensionConfig {
create?: Array<ContentActionRef>;
viewer?: {
openWith?: Array<ContentActionRef>;
actions?: Array<ContentActionRef>;
};
navbar?: Array<NavBarGroupRef>;
content?: {

View File

@ -36,6 +36,7 @@ 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';
import { NodePermissionService } from '../services/node-permission.service';
@Injectable()
export class ExtensionService implements RuleContext {
@ -52,6 +53,7 @@ export class ExtensionService implements RuleContext {
actions: Array<ActionRef> = [];
contentActions: Array<ContentActionRef> = [];
viewerActions: Array<ContentActionRef> = [];
openWithActions: Array<ContentActionRef> = [];
createActions: Array<ContentActionRef> = [];
navbar: Array<NavBarGroupRef> = [];
@ -63,7 +65,10 @@ export class ExtensionService implements RuleContext {
selection: SelectionState;
navigation: NavigationState;
constructor(private http: HttpClient, private store: Store<AppStore>) {
constructor(
private http: HttpClient,
private store: Store<AppStore>,
public permissions: NodePermissionService) {
this.evaluators = {
'core.every': core.every,
@ -118,6 +123,7 @@ export class ExtensionService implements RuleContext {
this.actions = this.loadActions(config);
this.routes = this.loadRoutes(config);
this.contentActions = this.loadContentActions(config);
this.viewerActions = this.loadViewerActions(config);
this.openWithActions = this.loadViewerOpenWith(config);
this.createActions = this.loadCreateActions(config);
this.navbar = this.loadNavBar(config);
@ -158,6 +164,15 @@ export class ExtensionService implements RuleContext {
return [];
}
protected loadViewerActions(config: ExtensionConfig) {
if (config && config.features && config.features.viewer) {
return (config.features.viewer.actions || []).sort(
this.sortByOrder
);
}
return [];
}
protected loadNavBar(config: ExtensionConfig): any {
if (config && config.features) {
return (config.features.navbar || [])
@ -296,7 +311,6 @@ export class ExtensionService implements RuleContext {
return this.contentActions
.filter(this.filterEnabled)
.filter(action => this.filterByRules(action))
.reduce(this.reduceSeparators, [])
.map(action => {
if (action.type === ContentActionType.menu) {
const copy = this.copyAction(action);
@ -311,7 +325,14 @@ export class ExtensionService implements RuleContext {
}
return action;
})
.reduce(this.reduceEmptyMenus, []);
.reduce(this.reduceEmptyMenus, [])
.reduce(this.reduceSeparators, []);
}
getViewerActions(): Array<ContentActionRef> {
return this.viewerActions
.filter(this.filterEnabled)
.filter(action => this.filterByRules(action));
}
reduceSeparators(
@ -320,6 +341,12 @@ export class ExtensionService implements RuleContext {
i: number,
arr: ContentActionRef[]
): ContentActionRef[] {
// remove leading separator
if (i === 0) {
if (arr[i].type === ContentActionType.separator) {
return acc;
}
}
// remove duplicate separators
if (i > 0) {
const prev = arr[i - 1];

View File

@ -23,14 +23,6 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-demo-button',
template: `
<button color="primary" mat-icon-button>
<mat-icon>extension</mat-icon>
</button>
`
})
export class DemoButtonComponent {}
export interface NodePermissions {
check(source: any, permissions: string[], options?: any): boolean;
}

View File

@ -25,6 +25,7 @@
import { SelectionState } from '../store/states';
import { NavigationState } from '../store/states/navigation.state';
import { NodePermissions } from './permission.extensions';
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
@ -32,6 +33,7 @@ export interface RuleContext {
selection: SelectionState;
navigation: NavigationState;
evaluators: { [key: string]: RuleEvaluator };
permissions: NodePermissions;
}
export class RuleRef {

View File

@ -39,7 +39,8 @@ import {
SearchRequest,
ResultSetPaging,
SiteBody,
SiteEntry
SiteEntry,
FavoriteBody
} from 'alfresco-js-api';
@Injectable()
@ -242,4 +243,30 @@ export class ContentApiService {
this.api.sitesApi.getSite(siteId, opts)
);
}
addFavorite(nodes: Array<MinimalNodeEntity>): Observable<any> {
const payload: FavoriteBody[] = nodes.map(node => {
const { isFolder, nodeId, id } = node.entry;
const siteId = node.entry['guid'];
const type = siteId ? 'site' : isFolder ? 'folder' : 'file';
const guid = siteId || nodeId || id;
return {
target: {
[type]: {
guid
}
}
};
});
return Observable.from(this.api.favoritesApi.addFavorite('-me-', <any>payload));
}
removeFavorite(nodes: Array<MinimalNodeEntity>): Observable<any> {
return Observable.from(Promise.all(nodes.map(node => {
const id = node.entry.nodeId || node.entry.id;
return this.api.favoritesApi.removeFavoriteSite('-me-', id);
})));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -25,11 +25,11 @@
import { Subject, Observable } from 'rxjs/Rx';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';
import { FolderDialogComponent, ConfirmDialogComponent } from '@alfresco/adf-content-services';
import { MatDialog, MatSnackBar } from '@angular/material';
import { FolderDialogComponent, ConfirmDialogComponent, ShareDialogComponent } from '@alfresco/adf-content-services';
import { LibraryDialogComponent } from '../dialogs/library/library.dialog';
import { SnackbarErrorAction, SnackbarInfoAction, SnackbarAction, SnackbarWarningAction,
NavigateRouteAction, SnackbarUserAction } from '../store/actions';
NavigateRouteAction, SnackbarUserAction, UndoDeleteNodesAction, SetSelectedNodesAction } from '../store/actions';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states';
import {
@ -43,6 +43,11 @@ import {
import { NodePermissionService } from './node-permission.service';
import { NodeInfo, DeletedNodeInfo, DeleteStatus } from '../store/models';
import { ContentApiService } from './content-api.service';
import { sharedUrl } from '../store/selectors/app.selectors';
import { NodeActionsService } from './node-actions.service';
import { TranslationService } from '@alfresco/adf-core';
import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog';
import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog';
interface RestoredNode {
status: number;
@ -61,14 +66,112 @@ export class ContentManagementService {
libraryDeleted = new Subject<string>();
libraryCreated = new Subject<SiteEntry>();
linksUnshared = new Subject<any>();
favoriteAdded = new Subject<Array<MinimalNodeEntity>>();
favoriteRemoved = new Subject<Array<MinimalNodeEntity>>();
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private permission: NodePermissionService,
private dialogRef: MatDialog
private dialogRef: MatDialog,
private nodeActionsService: NodeActionsService,
private translation: TranslationService,
private snackBar: MatSnackBar
) {}
addFavorite(nodes: Array<MinimalNodeEntity>) {
if (nodes && nodes.length > 0) {
this.contentApi.addFavorite(nodes).subscribe(() => {
nodes.forEach(node => {
node.entry.isFavorite = true;
});
this.store.dispatch(new SetSelectedNodesAction(nodes));
this.favoriteAdded.next(nodes);
});
}
}
removeFavorite(nodes: Array<MinimalNodeEntity>) {
if (nodes && nodes.length > 0) {
this.contentApi.removeFavorite(nodes).subscribe(() => {
nodes.forEach(node => {
node.entry.isFavorite = false;
});
this.store.dispatch(new SetSelectedNodesAction(nodes));
this.favoriteRemoved.next(nodes);
});
}
}
managePermissions(node: MinimalNodeEntity): void {
if (node && node.entry) {
const { nodeId, id } = node.entry;
const siteId = node.entry['guid'];
const targetId = siteId || nodeId || id;
if (targetId) {
this.dialogRef.open(NodePermissionsDialogComponent, {
data: { nodeId: targetId },
panelClass: 'aca-permissions-dialog-panel',
width: '730px'
});
} else {
this.store.dispatch(
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
);
}
}
}
manageVersions(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.nodeId) {
this.contentApi
.getNodeInfo(node.entry.nodeId)
.subscribe(entry => {
this.openVersionManagerDialog(entry);
});
} else {
this.openVersionManagerDialog(node.entry);
}
}
}
private openVersionManagerDialog(node: MinimalNodeEntryEntity) {
// workaround Shared
if (node.isFile || node.nodeId) {
this.dialogRef.open(NodeVersionsDialogComponent, {
data: { node },
panelClass: 'adf-version-manager-dialog-panel',
width: '630px'
});
} else {
this.store.dispatch(
new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')
);
}
}
shareNode(node: MinimalNodeEntity): void {
if (node && node.entry && node.entry.isFile) {
this.store
.select(sharedUrl)
.take(1)
.subscribe(baseShareUrl => {
this.dialogRef.open(ShareDialogComponent, {
width: '600px',
disableClose: true,
data: {
node,
baseShareUrl
}
});
});
}
}
createFolder(parentNodeId: string) {
const dialogInstance = this.dialogRef.open(FolderDialogComponent, {
data: {
@ -114,7 +217,7 @@ export class ContentManagementService {
}
createLibrary() {
const dialogInstance = this.dialogRef.open(LibraryDialogComponent, {
const dialogInstance = this.dialogRef.open(LibraryDialogComponent, {
width: '400px'
});
@ -129,12 +232,30 @@ export class ContentManagementService {
});
}
canDeleteNode(node: MinimalNodeEntity | Node): boolean {
return this.permission.check(node, ['delete']);
deleteLibrary(id: string): void {
this.contentApi.deleteSite(id).subscribe(
() => {
this.libraryDeleted.next(id);
this.store.dispatch(
new SnackbarInfoAction(
'APP.MESSAGES.INFO.LIBRARY_DELETED'
)
);
},
() => {
this.store.dispatch(
new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
)
);
}
);
}
canDeleteNodes(nodes: MinimalNodeEntity[]): boolean {
return this.permission.check(nodes, ['delete']);
async unshareNodes(links: Array<MinimalNodeEntity>) {
const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise());
await Promise.all(promises);
this.linksUnshared.next();
}
canUpdateNode(node: MinimalNodeEntity | Node): boolean {
@ -145,18 +266,6 @@ export class ContentManagementService {
return this.permission.check(folderNode, ['create']);
}
canDeleteSharedNodes(sharedLinks: MinimalNodeEntity[]): boolean {
return this.permission.check(sharedLinks, ['delete'], {
target: 'allowableOperationsOnTarget'
});
}
canUpdateSharedNode(sharedLink: MinimalNodeEntity): boolean {
return this.permission.check(sharedLink, ['update'], {
target: 'allowableOperationsOnTarget'
});
}
purgeDeletedNodes(nodes: MinimalNodeEntity[]) {
if (!nodes || nodes.length === 0) {
return;
@ -226,6 +335,271 @@ export class ContentManagementService {
});
}
copyNodes(nodes: Array<MinimalNodeEntity>) {
Observable.zip(
this.nodeActionsService.copyNodes(nodes),
this.nodeActionsService.contentCopied
).subscribe(
result => {
const [operationResult, newItems] = result;
this.showCopyMessage(operationResult, nodes, newItems);
},
error => {
this.showCopyMessage(error, nodes);
}
);
}
private showCopyMessage(
info: any,
nodes: Array<MinimalNodeEntity>,
newItems?: Array<MinimalNodeEntity>
) {
const numberOfCopiedItems = newItems ? newItems.length : 0;
const failedItems = nodes.length - numberOfCopiedItems;
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
if (typeof info === 'string') {
if (info.toLowerCase().indexOf('succes') !== -1) {
let i18MessageSuffix;
if (failedItems) {
if (numberOfCopiedItems) {
i18MessageSuffix =
numberOfCopiedItems === 1
? 'PARTIAL_SINGULAR'
: 'PARTIAL_PLURAL';
} else {
i18MessageSuffix =
failedItems === 1 ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
}
} else {
i18MessageSuffix =
numberOfCopiedItems === 1 ? 'SINGULAR' : 'PLURAL';
}
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
}
} else {
try {
const {
error: { statusCode }
} = JSON.parse(info.message);
if (statusCode === 403) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
} catch {}
}
const undo =
numberOfCopiedItems > 0
? this.translation.instant('APP.ACTIONS.UNDO')
: '';
const message = this.translation.instant(i18nMessageString, {
success: numberOfCopiedItems,
failed: failedItems
});
this.snackBar
.open(message, undo, {
panelClass: 'info-snackbar',
duration: 3000
})
.onAction()
.subscribe(() => this.undoCopyNodes(newItems));
}
private undoCopyNodes(nodes: MinimalNodeEntity[]) {
const batch = this.nodeActionsService
.flatten(nodes)
.filter(item => item.entry)
.map(item =>
this.contentApi.deleteNode(item.entry.id, { permanent: true })
);
Observable.forkJoin(...batch).subscribe(
() => {
this.nodesDeleted.next(null);
},
error => {
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
let errorJson = null;
try {
errorJson = JSON.parse(error.message);
} catch {}
if (
errorJson &&
errorJson.error &&
errorJson.error.statusCode === 403
) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
this.store.dispatch(new SnackbarErrorAction(i18nMessageString));
}
);
}
moveNodes(nodes: Array<MinimalNodeEntity>) {
const permissionForMove = '!';
Observable.zip(
this.nodeActionsService.moveNodes(nodes, permissionForMove),
this.nodeActionsService.contentMoved
).subscribe(
(result) => {
const [ operationResult, moveResponse ] = result;
this.showMoveMessage(nodes, operationResult, moveResponse);
this.nodesMoved.next(null);
},
(error) => {
this.showMoveMessage(nodes, error);
}
);
}
private undoMoveNodes(moveResponse, selectionParentId) {
const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : [];
const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : [];
const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries
.map((folderEntry) => {
return this.contentApi
.restoreNode(folderEntry.nodeId || folderEntry.id)
.map(node => node.entry);
});
Observable.zip(...restoreDeletedNodesBatch, Observable.of(null))
.flatMap(() => {
const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes];
const revertMoveBatch = this.nodeActionsService
.flatten(nodesToBeMovedBack)
.filter(node => node.entry || (node.itemMoved && node.itemMoved.entry))
.map((node) => {
if (node.itemMoved) {
return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId);
} else {
return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId);
}
});
return Observable.zip(...revertMoveBatch, Observable.of(null));
})
.subscribe(
() => {
this.nodesMoved.next(null);
},
error => {
let message = 'APP.MESSAGES.ERRORS.GENERIC';
let errorJson = null;
try {
errorJson = JSON.parse(error.message);
} catch {}
if (errorJson && errorJson.error && errorJson.error.statusCode === 403) {
message = 'APP.MESSAGES.ERRORS.PERMISSION';
}
this.store.dispatch(new SnackbarErrorAction(message));
}
);
}
deleteNodes(items: MinimalNodeEntity[]): void {
const batch: Observable<DeletedNodeInfo>[] = [];
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.nodesDeleted.next();
}
});
}
undoDeleteNodes(items: DeletedNodeInfo[]): void {
const batch: Observable<DeletedNodeInfo>[] = [];
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.nodesRestored.next();
}
});
}
private undoDeleteNode(item: DeletedNodeInfo): Observable<DeletedNodeInfo> {
const { id, name } = item;
return this.contentApi
.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 restoreNode(node: MinimalNodeEntity): Observable<RestoredNode> {
const { entry } = node;
@ -457,4 +831,170 @@ export class ContentManagementService {
}
});
}
private deleteNode(node: MinimalNodeEntity): Observable<DeletedNodeInfo> {
const { name } = node.entry;
const id = node.entry.nodeId || node.entry.id;
return this.contentApi
.deleteNode(id)
.map(() => {
return {
id,
name,
status: 1
};
})
.catch(() => {
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 showMoveMessage(nodes: Array<MinimalNodeEntity>, info: any, moveResponse?: any) {
const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0;
const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0;
const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0;
let successMessage = '';
let partialSuccessMessage = '';
let failedMessage = '';
let errorMessage = '';
if (typeof info === 'string') {
// in case of success
if (info.toLowerCase().indexOf('succes') !== -1) {
const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.';
let i18MessageSuffix = '';
if (succeeded) {
i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL';
successMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
if (partiallySucceeded) {
i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL';
partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
if (failures) {
// if moving failed for ALL nodes, emit error
if (failures === nodes.length) {
const errors = this.nodeActionsService.flatten(moveResponse['failed']);
errorMessage = this.getErrorMessage(errors[0]);
} else {
i18MessageSuffix = 'PARTIAL.FAIL';
failedMessage = `${i18nMessageString}${i18MessageSuffix}`;
}
}
} else {
errorMessage = 'APP.MESSAGES.ERRORS.GENERIC';
}
} else {
errorMessage = this.getErrorMessage(info);
}
const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : '';
failedMessage = errorMessage ? errorMessage : failedMessage;
const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : '';
const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : '';
const initialParentId = this.nodeActionsService.getEntryParentId(nodes[0].entry);
const messages = this.translation.instant(
[successMessage, partialSuccessMessage, failedMessage],
{ success: succeeded, failed: failures, partially: partiallySucceeded}
);
// TODO: review in terms of i18n
this.snackBar
.open(
messages[successMessage]
+ beforePartialSuccessMessage + messages[partialSuccessMessage]
+ beforeFailedMessage + messages[failedMessage]
, undo, {
panelClass: 'info-snackbar',
duration: 3000
})
.onAction()
.subscribe(() => this.undoMoveNodes(moveResponse, initialParentId));
}
getErrorMessage(errorObject): string {
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
try {
const { error: { statusCode } } = JSON.parse(errorObject.message);
if (statusCode === 409) {
i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE';
} else if (statusCode === 403) {
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
}
} catch (err) { /* Do nothing, keep the original message */ }
return i18nMessageString;
}
}

View File

@ -24,9 +24,10 @@
*/
import { Injectable } from '@angular/core';
import { NodePermissions } from '../extensions/permission.extensions';
@Injectable()
export class NodePermissionService {
export class NodePermissionService implements NodePermissions {
static DEFAULT_OPERATION = 'OR';
private defaultOptions = {
@ -34,8 +35,8 @@ export class NodePermissionService {
target: null
};
check(source: any, permissions: string[], options: any = {}): boolean {
const opts = Object.assign({}, this.defaultOptions, options);
check(source: any, permissions: string[], options?: any): boolean {
const opts = Object.assign({}, this.defaultOptions, options || {});
if (source) {
if (Array.isArray(source) && source.length) {

View File

@ -24,6 +24,7 @@
*/
export * from './actions/app.actions';
export * from './actions/favorite.actions';
export * from './actions/node.actions';
export * from './actions/snackbar.actions';
export * from './actions/router.actions';
@ -31,3 +32,4 @@ export * from './actions/viewer.actions';
export * from './actions/search.actions';
export * from './actions/user.actions';
export * from './actions/library.actions';
export * from './actions/upload.actions';

View File

@ -33,6 +33,8 @@ export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER';
export const SET_SHARED_URL = 'SET_SHARED_URL';
export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER';
export const SET_CURRENT_URL = 'SET_CURRENT_URL';
export const TOGGLE_INFO_DRAWER = 'TOGGLE_INFO_DRAWER';
export const TOGGLE_DOCUMENT_DISPLAY_MODE = 'TOGGLE_DOCUMENT_DISPLAY_MODE';
export class SetAppNameAction implements Action {
readonly type = SET_APP_NAME;
@ -68,3 +70,13 @@ export class SetCurrentUrlAction implements Action {
readonly type = SET_CURRENT_URL;
constructor(public payload: string) {}
}
export class ToggleInfoDrawerAction implements Action {
readonly type = TOGGLE_INFO_DRAWER;
constructor(public payload?: any) {}
}
export class ToggleDocumentDisplayMode implements Action {
readonly type = TOGGLE_DOCUMENT_DISPLAY_MODE;
constructor(public payload?: any) {}
}

View File

@ -23,23 +23,18 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { Action } from '@ngrx/store';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states';
import { RestoreDeletedNodesAction } from '../store/actions';
@Directive({
selector: '[acaRestoreNode]'
})
export class NodeRestoreDirective {
// tslint:disable-next-line:no-input-rename
@Input('acaRestoreNode') selection: MinimalNodeEntity[];
export const ADD_FAVORITE = 'ADD_FAVORITE';
export const REMOVE_FAVORITE = 'REMOVE_FAVORITE';
constructor(private store: Store<AppStore>) {}
@HostListener('click')
onClick() {
this.store.dispatch(new RestoreDeletedNodesAction(this.selection));
}
export class AddFavoriteAction implements Action {
readonly type = ADD_FAVORITE;
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class RemoveFavoriteAction implements Action {
readonly type = REMOVE_FAVORITE;
constructor(public payload: Array<MinimalNodeEntity>) {}
}

View File

@ -30,7 +30,7 @@ export const CREATE_LIBRARY = 'CREATE_LIBRARY';
export class DeleteLibraryAction implements Action {
readonly type = DELETE_LIBRARY;
constructor(public payload: string) {}
constructor(public payload?: string) {}
}
export class CreateLibraryAction implements Action {

View File

@ -24,7 +24,6 @@
*/
import { Action } from '@ngrx/store';
import { NodeInfo } from '../models';
import { MinimalNodeEntity } from 'alfresco-js-api';
export const SET_SELECTED_NODES = 'SET_SELECTED_NODES';
@ -35,6 +34,12 @@ export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES';
export const DOWNLOAD_NODES = 'DOWNLOAD_NODES';
export const CREATE_FOLDER = 'CREATE_FOLDER';
export const EDIT_FOLDER = 'EDIT_FOLDER';
export const SHARE_NODE = 'SHARE_NODE';
export const UNSHARE_NODES = 'UNSHARE_NODES';
export const COPY_NODES = 'COPY_NODES';
export const MOVE_NODES = 'MOVE_NODES';
export const MANAGE_PERMISSIONS = 'MANAGE_PERMISSIONS';
export const MANAGE_VERSIONS = 'MANAGE_VERSIONS';
export class SetSelectedNodesAction implements Action {
readonly type = SET_SELECTED_NODES;
@ -43,7 +48,7 @@ export class SetSelectedNodesAction implements Action {
export class DeleteNodesAction implements Action {
readonly type = DELETE_NODES;
constructor(public payload: NodeInfo[] = []) {}
constructor(public payload: MinimalNodeEntity[] = []) {}
}
export class UndoDeleteNodesAction implements Action {
@ -75,3 +80,33 @@ export class EditFolderAction implements Action {
readonly type = EDIT_FOLDER;
constructor(public payload: MinimalNodeEntity) {}
}
export class ShareNodeAction implements Action {
readonly type = SHARE_NODE;
constructor(public payload: MinimalNodeEntity) {}
}
export class UnshareNodesAction implements Action {
readonly type = UNSHARE_NODES;
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class CopyNodesAction implements Action {
readonly type = COPY_NODES;
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class MoveNodesAction implements Action {
readonly type = MOVE_NODES;
constructor(public payload: Array<MinimalNodeEntity>) {}
}
export class ManagePermissionsAction implements Action {
readonly type = MANAGE_PERMISSIONS;
constructor(public payload: MinimalNodeEntity) {}
}
export class ManageVersionsAction implements Action {
readonly type = MANAGE_VERSIONS;
constructor(public payload: MinimalNodeEntity) {}
}

View File

@ -23,27 +23,17 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Store } from '@ngrx/store';
import { AppStore } from '../store/states';
import { PurgeDeletedNodesAction } from '../store/actions';
import { Action } from '@ngrx/store';
@Directive({
selector: '[acaPermanentDelete]'
})
export class NodePermanentDeleteDirective {
export const UPLOAD_FILES = 'UPLOAD_FILES';
export const UPLOAD_FOLDER = 'UPLOAD_FOLDER';
// tslint:disable-next-line:no-input-rename
@Input('acaPermanentDelete')
selection: MinimalNodeEntity[];
constructor(
private store: Store<AppStore>
) {}
@HostListener('click')
onClick() {
this.store.dispatch(new PurgeDeletedNodesAction(this.selection));
}
export class UploadFilesAction implements Action {
readonly type = UPLOAD_FILES;
constructor(public payload: any) {}
}
export class UploadFolderAction implements Action {
readonly type = UPLOAD_FOLDER;
constructor(public payload: any) {}
}

View File

@ -38,7 +38,9 @@ import {
DownloadEffects,
ViewerEffects,
SearchEffects,
SiteEffects
SiteEffects,
UploadEffects,
FavoriteEffects
} from './effects';
@NgModule({
@ -55,7 +57,9 @@ import {
DownloadEffects,
ViewerEffects,
SearchEffects,
SiteEffects
SiteEffects,
UploadEffects,
FavoriteEffects
]),
!environment.production
? StoreDevtoolsModule.instrument({ maxAge: 25 })

View File

@ -24,9 +24,11 @@
*/
export * from './effects/download.effects';
export * from './effects/favorite.effects';
export * from './effects/node.effects';
export * from './effects/router.effects';
export * from './effects/snackbar.effects';
export * from './effects/viewer.effects';
export * from './effects/search.effects';
export * from './effects/library.effects';
export * from './effects/upload.effects';

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { ADD_FAVORITE, AddFavoriteAction, RemoveFavoriteAction, REMOVE_FAVORITE } from '../actions/favorite.actions';
import { Store } from '@ngrx/store';
import { AppStore } from '../states';
import { appSelection } from '../selectors/app.selectors';
import { ContentManagementService } from '../../services/content-management.service';
@Injectable()
export class FavoriteEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private content: ContentManagementService
) {}
@Effect({ dispatch: false })
addFavorite$ = this.actions$.pipe(
ofType<AddFavoriteAction>(ADD_FAVORITE),
map(action => {
if (action.payload && action.payload.length > 0) {
this.content.addFavorite(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.content.addFavorite(selection.nodes);
}
});
}
})
);
@Effect({ dispatch: false })
removeFavorite$ = this.actions$.pipe(
ofType<RemoveFavoriteAction>(REMOVE_FAVORITE),
map(action => {
if (action.payload && action.payload.length > 0) {
this.content.removeFavorite(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.content.removeFavorite(selection.nodes);
}
});
}
})
);
}

View File

@ -30,21 +30,16 @@ import {
DeleteLibraryAction, DELETE_LIBRARY,
CreateLibraryAction, CREATE_LIBRARY
} from '../actions';
import {
SnackbarInfoAction,
SnackbarErrorAction
} from '../actions/snackbar.actions';
import { Store } from '@ngrx/store';
import { AppStore } from '../states/app.state';
import { ContentManagementService } from '../../services/content-management.service';
import { ContentApiService } from '../../services/content-api.service';
import { Store } from '@ngrx/store';
import { AppStore } from '../states';
import { appSelection } from '../selectors/app.selectors';
@Injectable()
export class SiteEffects {
constructor(
private actions$: Actions,
private store: Store<AppStore>,
private contentApi: ContentApiService,
private actions$: Actions,
private content: ContentManagementService
) {}
@ -53,7 +48,16 @@ export class SiteEffects {
ofType<DeleteLibraryAction>(DELETE_LIBRARY),
map(action => {
if (action.payload) {
this.deleteLibrary(action.payload);
this.content.deleteLibrary(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.library) {
this.content.deleteLibrary(selection.library.entry.id);
}
});
}
})
);
@ -62,31 +66,7 @@ export class SiteEffects {
createLibrary$ = this.actions$.pipe(
ofType<CreateLibraryAction>(CREATE_LIBRARY),
map(action => {
this.createLibrary();
this.content.createLibrary();
})
);
private deleteLibrary(id: string) {
this.contentApi.deleteSite(id).subscribe(
() => {
this.content.libraryDeleted.next(id);
this.store.dispatch(
new SnackbarInfoAction(
'APP.MESSAGES.INFO.LIBRARY_DELETED'
)
);
},
() => {
this.store.dispatch(
new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
)
);
}
);
}
private createLibrary() {
this.content.createLibrary();
}
}

View File

@ -29,49 +29,97 @@ import { map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppStore } from '../states/app.state';
import {
SnackbarWarningAction,
SnackbarInfoAction,
SnackbarErrorAction,
PurgeDeletedNodesAction,
PURGE_DELETED_NODES,
DeleteNodesAction,
DELETE_NODES,
SnackbarUserAction,
SnackbarAction,
UndoDeleteNodesAction,
UNDO_DELETE_NODES,
CreateFolderAction,
CREATE_FOLDER
CREATE_FOLDER,
EditFolderAction,
EDIT_FOLDER,
RestoreDeletedNodesAction,
RESTORE_DELETED_NODES,
ShareNodeAction,
SHARE_NODE
} from '../actions';
import { ContentManagementService } from '../../services/content-management.service';
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, RestoreDeletedNodesAction, RESTORE_DELETED_NODES } from '../actions/node.actions';
import {
UnshareNodesAction,
UNSHARE_NODES,
CopyNodesAction,
COPY_NODES,
MoveNodesAction,
MOVE_NODES,
ManagePermissionsAction,
MANAGE_PERMISSIONS,
ManageVersionsAction,
MANAGE_VERSIONS
} from '../actions/node.actions';
@Injectable()
export class NodeEffects {
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private contentManagementService: ContentManagementService,
private contentApi: ContentApiService
private contentService: ContentManagementService
) {}
@Effect({ dispatch: false })
shareNode$ = this.actions$.pipe(
ofType<ShareNodeAction>(SHARE_NODE),
map(action => {
if (action.payload) {
this.contentService.shareNode(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.file) {
this.contentService.shareNode(selection.file);
}
});
}
})
);
@Effect({ dispatch: false })
unshareNodes$ = this.actions$.pipe(
ofType<UnshareNodesAction>(UNSHARE_NODES),
map(action => {
if (action && action.payload && action.payload.length > 0) {
this.contentService.unshareNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.contentService.unshareNodes(selection.nodes);
}
});
}
})
);
@Effect({ dispatch: false })
purgeDeletedNodes$ = this.actions$.pipe(
ofType<PurgeDeletedNodesAction>(PURGE_DELETED_NODES),
map(action => {
if (action && action.payload && action.payload.length > 0) {
this.contentManagementService.purgeDeletedNodes(action.payload);
this.contentService.purgeDeletedNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.count > 0) {
this.contentManagementService.purgeDeletedNodes(selection.nodes);
this.contentService.purgeDeletedNodes(
selection.nodes
);
}
});
}
@ -83,14 +131,16 @@ export class NodeEffects {
ofType<RestoreDeletedNodesAction>(RESTORE_DELETED_NODES),
map(action => {
if (action && action.payload && action.payload.length > 0) {
this.contentManagementService.restoreDeletedNodes(action.payload);
this.contentService.restoreDeletedNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.count > 0) {
this.contentManagementService.restoreDeletedNodes(selection.nodes);
this.contentService.restoreDeletedNodes(
selection.nodes
);
}
});
}
@ -101,8 +151,17 @@ export class NodeEffects {
deleteNodes$ = this.actions$.pipe(
ofType<DeleteNodesAction>(DELETE_NODES),
map(action => {
if (action.payload.length > 0) {
this.deleteNodes(action.payload);
if (action && action.payload && action.payload.length > 0) {
this.contentService.deleteNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.count > 0) {
this.contentService.deleteNodes(selection.nodes);
}
});
}
})
);
@ -112,7 +171,7 @@ export class NodeEffects {
ofType<UndoDeleteNodesAction>(UNDO_DELETE_NODES),
map(action => {
if (action.payload.length > 0) {
this.undoDeleteNodes(action.payload);
this.contentService.undoDeleteNodes(action.payload);
}
})
);
@ -122,14 +181,14 @@ export class NodeEffects {
ofType<CreateFolderAction>(CREATE_FOLDER),
map(action => {
if (action.payload) {
this.contentManagementService.createFolder(action.payload);
this.contentService.createFolder(action.payload);
} else {
this.store
.select(currentFolder)
.take(1)
.subscribe(node => {
if (node && node.id) {
this.contentManagementService.createFolder(node.id);
this.contentService.createFolder(node.id);
}
});
}
@ -141,215 +200,97 @@ export class NodeEffects {
ofType<EditFolderAction>(EDIT_FOLDER),
map(action => {
if (action.payload) {
this.contentManagementService.editFolder(action.payload);
this.contentService.editFolder(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.folder) {
this.contentManagementService.editFolder(selection.folder);
this.contentService.editFolder(selection.folder);
}
});
}
})
);
private deleteNodes(items: NodeInfo[]): void {
const batch: Observable<DeletedNodeInfo>[] = [];
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<DeletedNodeInfo> {
const { id, name } = node;
return this.contentApi
.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<DeletedNodeInfo>[] = [];
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<DeletedNodeInfo> {
const { id, name } = item;
return this.contentApi
.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 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);
@Effect({ dispatch: false })
copyNodes$ = this.actions$.pipe(
ofType<CopyNodesAction>(COPY_NODES),
map(action => {
if (action.payload && action.payload.length > 0) {
this.contentService.copyNodes(action.payload);
} else {
acc.fail.push(node);
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.contentService.copyNodes(selection.nodes);
}
});
}
})
);
return acc;
}, status);
}
@Effect({ dispatch: false })
moveNodes$ = this.actions$.pipe(
ofType<MoveNodesAction>(MOVE_NODES),
map(action => {
if (action.payload && action.payload.length > 0) {
this.contentService.moveNodes(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.contentService.moveNodes(selection.nodes);
}
});
}
})
);
@Effect({ dispatch: false })
managePermissions = this.actions$.pipe(
ofType<ManagePermissionsAction>(MANAGE_PERMISSIONS),
map(action => {
if (action && action.payload) {
this.contentService.managePermissions(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && !selection.isEmpty) {
this.contentService.managePermissions(
selection.first
);
}
});
}
})
);
@Effect({ dispatch: false })
manageVersions$ = this.actions$.pipe(
ofType<ManageVersionsAction>(MANAGE_VERSIONS),
map(action => {
if (action && action.payload) {
this.contentService.manageVersions(action.payload);
} else {
this.store
.select(appSelection)
.take(1)
.subscribe(selection => {
if (selection && selection.file) {
this.contentService.manageVersions(
selection.file
);
}
});
}
})
);
}

View File

@ -0,0 +1,116 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { Injectable, RendererFactory2, NgZone } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { AppStore } from '../states';
import { UploadFilesAction, UPLOAD_FILES } from '../actions';
import { map } from 'rxjs/operators';
import { FileUtils, FileModel, UploadService } from '@alfresco/adf-core';
import { currentFolder } from '../selectors/app.selectors';
import { UploadFolderAction, UPLOAD_FOLDER } from '../actions/upload.actions';
@Injectable()
export class UploadEffects {
private fileInput: HTMLInputElement;
private folderInput: HTMLInputElement;
constructor(
private store: Store<AppStore>,
private actions$: Actions,
private ngZone: NgZone,
private uploadService: UploadService,
rendererFactory: RendererFactory2
) {
const renderer = rendererFactory.createRenderer(null, null);
this.fileInput = renderer.createElement('input') as HTMLInputElement;
this.fileInput.id = 'app-upload-files';
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.fileInput.setAttribute('multiple', '');
this.fileInput.addEventListener('change', event => this.upload(event));
renderer.appendChild(document.body, this.fileInput);
this.folderInput = renderer.createElement('input') as HTMLInputElement;
this.folderInput.id = 'app-upload-folder';
this.folderInput.type = 'file';
this.folderInput.style.display = 'none';
this.folderInput.setAttribute('directory', '');
this.folderInput.setAttribute('webkitdirectory', '');
this.folderInput.addEventListener('change', event => this.upload(event));
renderer.appendChild(document.body, this.folderInput);
}
@Effect({ dispatch: false })
uploadFiles$ = this.actions$.pipe(
ofType<UploadFilesAction>(UPLOAD_FILES),
map(() => {
this.fileInput.click();
})
);
@Effect({ dispatch: false })
uploadFolder$ = this.actions$.pipe(
ofType<UploadFolderAction>(UPLOAD_FOLDER),
map(() => {
this.folderInput.click();
})
);
private upload(event: any): void {
this.store
.select(currentFolder)
.take(1)
.subscribe(node => {
if (node && node.id) {
const input = <HTMLInputElement>event.currentTarget;
const files = FileUtils.toFileArray(input.files).map(
file => {
return new FileModel(file, {
parentId: node.id,
path: (file.webkitRelativePath || '').replace(/\/[^\/]*$/, ''),
nodeType: 'cm:content'
});
}
);
this.uploadQueue(files);
event.target.value = '';
}
});
}
private uploadQueue(files: FileModel[]) {
if (files.length > 0) {
this.ngZone.run(() => {
this.uploadService.addToQueue(...files);
this.uploadService.uploadFilesInTheQueue();
});
}
}
}

View File

@ -42,8 +42,15 @@ import {
SetSharedUrlAction,
SET_CURRENT_FOLDER,
SetCurrentFolderAction,
SET_CURRENT_URL, SetCurrentUrlAction
SET_CURRENT_URL,
SetCurrentUrlAction
} from '../actions';
import {
TOGGLE_INFO_DRAWER,
ToggleInfoDrawerAction,
TOGGLE_DOCUMENT_DISPLAY_MODE,
ToggleDocumentDisplayMode
} from '../actions/app.actions';
export function appReducer(
state: AppState = INITIAL_APP_STATE,
@ -85,6 +92,14 @@ export function appReducer(
case SET_CURRENT_URL:
newState = updateCurrentUrl(state, <SetCurrentUrlAction>action);
break;
case TOGGLE_INFO_DRAWER:
newState = updateInfoDrawer(state, <ToggleInfoDrawerAction>action);
break;
case TOGGLE_DOCUMENT_DISPLAY_MODE:
newState = updateDocumentDisplayMode(state, <
ToggleDocumentDisplayMode
>action);
break;
default:
newState = Object.assign({}, state);
}
@ -168,6 +183,31 @@ function updateCurrentUrl(state: AppState, action: SetCurrentUrlAction) {
return newState;
}
function updateInfoDrawer(state: AppState, action: ToggleInfoDrawerAction) {
const newState = Object.assign({}, state);
let value = state.infoDrawerOpened;
if (state.selection.isEmpty) {
value = false;
} else {
value = !value;
}
newState.infoDrawerOpened = value;
return newState;
}
function updateDocumentDisplayMode(
state: AppState,
action: ToggleDocumentDisplayMode
) {
const newState = Object.assign({}, state);
newState.documentDisplayMode =
newState.documentDisplayMode === 'list' ? 'gallery' : 'list';
return newState;
}
function updateSelectedNodes(
state: AppState,
action: SetSelectedNodesAction
@ -181,6 +221,7 @@ function updateSelectedNodes(
let last = null;
let file = null;
let folder = null;
let library = null;
if (nodes.length > 0) {
first = nodes[0];
@ -197,6 +238,15 @@ function updateSelectedNodes(
}
}
const libraries = [...action.payload].filter((node: any) => node.isLibrary);
if (libraries.length === 1) {
library = libraries[0];
}
if (isEmpty) {
newState.infoDrawerOpened = false;
}
newState.selection = {
count,
nodes,
@ -204,7 +254,9 @@ function updateSelectedNodes(
first,
last,
file,
folder
folder,
libraries,
library
};
return newState;
}

View File

@ -36,6 +36,8 @@ export const selectUser = createSelector(selectApp, state => state.user);
export const sharedUrl = createSelector(selectApp, state => state.sharedUrl);
export const appNavigation = createSelector(selectApp, state => state.navigation);
export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder);
export const infoDrawerOpened = createSelector(selectApp, state => state.infoDrawerOpened);
export const documentDisplayMode = createSelector(selectApp, state => state.documentDisplayMode);
export const selectionWithFolder = createSelector(
appSelection,

View File

@ -36,6 +36,8 @@ export interface AppState {
selection: SelectionState;
user: ProfileState;
navigation: NavigationState;
infoDrawerOpened: boolean;
documentDisplayMode: string;
}
export const INITIAL_APP_STATE: AppState = {
@ -52,12 +54,15 @@ export const INITIAL_APP_STATE: AppState = {
},
selection: {
nodes: [],
libraries: [],
isEmpty: true,
count: 0
},
navigation: {
currentFolder: null
}
},
infoDrawerOpened: false,
documentDisplayMode: 'list'
};
export interface AppStore {

View File

@ -23,14 +23,16 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { MinimalNodeEntity } from 'alfresco-js-api';
import { MinimalNodeEntity, SiteEntry } from 'alfresco-js-api';
export interface SelectionState {
count: number;
nodes: MinimalNodeEntity[];
libraries: SiteEntry[];
isEmpty: boolean;
first?: MinimalNodeEntity;
last?: MinimalNodeEntity;
folder?: MinimalNodeEntity;
file?: MinimalNodeEntity;
library?: SiteEntry;
}

View File

@ -8,6 +8,85 @@
],
"rules": [
{
"id": "app.toolbar.favorite.canToggle",
"comment": "workaround for recent files and search api issue",
"type": "core.every",
"parameters": [
{
"type": "rule",
"value": "core.some",
"parameters": [
{ "type": "rule", "value": "app.selection.canAddFavorite" },
{ "type": "rule", "value": "app.selection.canRemoveFavorite" }
]
},
{
"type": "rule",
"value": "core.some",
"parameters": [
{ "type": "rule", "value": "app.navigation.isRecentFiles" },
{ "type": "rule", "value": "app.navigation.isSharedFiles" },
{ "type": "rule", "value": "app.navigation.isSearchResults" }
]
}
]
},
{
"id": "app.toolbar.favorite.canAdd",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.canAddFavorite" },
{ "type": "rule", "value": "app.navigation.isNotRecentFiles" },
{ "type": "rule", "value": "app.navigation.isNotSharedFiles" },
{ "type": "rule", "value": "app.navigation.isNotSearchResults" }
]
},
{
"id": "app.toolbar.favorite.canRemove",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.canRemoveFavorite" },
{ "type": "rule", "value": "app.navigation.isNotRecentFiles" },
{ "type": "rule", "value": "app.navigation.isNotSharedFiles" },
{ "type": "rule", "value": "app.navigation.isNotSearchResults" }
]
},
{
"id": "app.toolbar.info",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isNotLibraries" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
]
},
{
"id": "app.toolbar.canCopyNode",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" },
{ "type": "rule", "value": "app.navigation.isNotLibraries" }
]
},
{
"id": "app.toolbar.permissions",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.file" },
{ "type": "rule", "value": "app.selection.first.canUpdate" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
]
},
{
"id": "app.toolbar.versions",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.file" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
]
},
{
"id": "app.trashcan.hasSelection",
"type": "core.every",
@ -21,7 +100,8 @@
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.folder" },
{ "type": "rule", "value": "app.selection.folder.canUpdate" }
{ "type": "rule", "value": "app.selection.folder.canUpdate" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
]
},
{
@ -57,13 +137,43 @@
"id": "app.create.folder",
"type": "default",
"icon": "create_new_folder",
"title": "ext: Create Folder",
"title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
"description-disabled": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED",
"actions": {
"click": "CREATE_FOLDER"
},
"rules": {
"enabled": "app.navigation.folder.canCreate"
}
},
{
"id": "app.create.uploadFile",
"type": "default",
"icon": "file_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES",
"description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED",
"actions": {
"click": "UPLOAD_FILES"
},
"rules": {
"enabled": "app.navigation.folder.canUpload"
}
},
{
"id": "app.create.uploadFolder",
"type": "default",
"icon": "file_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS",
"description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED",
"actions": {
"click": "UPLOAD_FOLDER"
},
"rules": {
"enabled": "app.navigation.folder.canUpload"
}
}
],
"navbar": [
@ -122,24 +232,6 @@
],
"content": {
"actions": [
{
"id": "app.toolbar.separator.1",
"order": 5,
"type": "separator"
},
{
"id": "app.toolbar.createFolder",
"type": "button",
"order": 10,
"title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
"icon": "create_new_folder",
"actions": {
"click": "CREATE_FOLDER"
},
"rules": {
"visible": "app.navigation.folder.canCreate"
}
},
{
"id": "app.toolbar.preview",
"type": "button",
@ -204,16 +296,261 @@
}
},
{
"id": "app.toolbar.separator.2",
"order": 200,
"type": "separator"
"id": "app.toolbar.createLibrary",
"type": "button",
"title": "Create Library",
"icon": "create_new_folder",
"actions": {
"click": "CREATE_LIBRARY"
},
"rules": {
"visible": "app.navigation.isLibraries"
}
},
{
"disabled": true,
"id": "app.toolbar.custom.1",
"order": 200,
"id": "app.toolbar.info",
"type": "custom",
"component": "app.demo.button"
"component": "app.toolbar.toggleInfoDrawer",
"rules": {
"visible": "app.toolbar.info"
}
},
{
"id": "app.toolbar.more",
"type": "menu",
"icon": "more_vert",
"title": "APP.ACTIONS.MORE",
"children": [
{
"id": "app.toolbar.favorite",
"comment": "workaround for Recent Files and Search API issue",
"type": "custom",
"component": "app.toolbar.toggleFavorite",
"rules": {
"visible": "app.toolbar.favorite.canToggle"
}
},
{
"id": "app.toolbar.favorite.add",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"icon": "star_border",
"actions": {
"click": "ADD_FAVORITE"
},
"rules": {
"visible": "app.toolbar.favorite.canAdd"
}
},
{
"id": "app.toolbar.favorite.remove",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"icon": "star",
"actions": {
"click": "REMOVE_FAVORITE"
},
"rules": {
"visible": "app.toolbar.favorite.canRemove"
}
},
{
"id": "app.toolbar.copy",
"type": "button",
"title": "APP.ACTIONS.COPY",
"icon": "content_copy",
"actions": {
"click": "COPY_NODES"
},
"rules": {
"visible": "app.toolbar.canCopyNode"
}
},
{
"id": "app.toolbar.move",
"type": "button",
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"actions": {
"click": "MOVE_NODES"
},
"rules": {
"visible": "app.selection.canDelete"
}
},
{
"id": "app.toolbar.share",
"type": "button",
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"actions": {
"click": "SHARE_NODE"
},
"rules": {
"visible": "app.selection.file.canShare"
}
},
{
"id": "app.toolbar.unshare",
"type": "button",
"title": "APP.ACTIONS.UNSHARE",
"icon": "stop_screen_share",
"actions": {
"click": "UNSHARE_NODES"
},
"rules": {
"visible": "app.selection.canUnshare"
}
},
{
"id": "app.toolbar.delete",
"type": "button",
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
"click": "DELETE_NODES"
},
"rules": {
"visible": "app.selection.canDelete"
}
},
{
"id": "app.toolbar.deleteLibrary",
"type": "button",
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
"click": "DELETE_LIBRARY"
},
"rules": {
"visible": "app.selection.library"
}
},
{
"id": "app.toolbar.versions",
"type": "button",
"title": "APP.ACTIONS.VERSIONS",
"icon": "history",
"actions": {
"click": "MANAGE_VERSIONS"
},
"rules": {
"visible": "app.toolbar.versions"
}
},
{
"id": "app.toolbar.permissions",
"type": "button",
"title": "APP.ACTIONS.PERMISSIONS",
"icon": "settings_input_component",
"actions": {
"click": "MANAGE_PERMISSIONS"
},
"rules": {
"visible": "app.toolbar.permissions"
}
}
]
}
]
},
"viewer": {
"actions": [
{
"id": "app.viewer.favorite.add",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"icon": "star_border",
"actions": {
"click": "ADD_FAVORITE"
},
"rules": {
"visible": "app.toolbar.favorite.canAdd"
}
},
{
"id": "app.viewer.favorite.remove",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"icon": "star",
"actions": {
"click": "REMOVE_FAVORITE"
},
"rules": {
"visible": "app.toolbar.favorite.canRemove"
}
},
{
"id": "app.viewer.share",
"type": "button",
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"actions": {
"click": "SHARE_NODE"
},
"rules": {
"visible": "app.selection.file.canShare"
}
},
{
"id": "app.viewer.copy",
"type": "button",
"title": "APP.ACTIONS.COPY",
"icon": "content_copy",
"actions": {
"click": "COPY_NODES"
},
"rules": {
"visible": "app.toolbar.canCopyNode"
}
},
{
"id": "app.viewer.move",
"type": "button",
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"actions": {
"click": "MOVE_NODES"
},
"rules": {
"visible": "app.selection.canDelete"
}
},
{
"id": "app.viewer.delete",
"type": "button",
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
"click": "DELETE_NODES"
},
"rules": {
"visible": "app.selection.canDelete"
}
},
{
"id": "app.viewer.versions",
"type": "button",
"title": "APP.ACTIONS.VERSIONS",
"icon": "history",
"actions": {
"click": "MANAGE_VERSIONS"
},
"rules": {
"visible": "app.toolbar.versions"
}
},
{
"id": "app.viewer.permissions",
"type": "button",
"title": "APP.ACTIONS.PERMISSIONS",
"icon": "settings_input_component",
"actions": {
"click": "MANAGE_PERMISSIONS"
},
"rules": {
"visible": "app.toolbar.permissions"
}
}
]
}

View File

@ -26,11 +26,11 @@
},
"TOOLTIPS": {
"CREATE_FOLDER": "Create new folder",
"CREATE_FOLDER_NOT_ALLOWED": "You can't create a folder here. You might not have the required permissions, check with your IT Team.",
"CREATE_FOLDER_NOT_ALLOWED": "Folders cannot be created whilst viewing the current items.",
"UPLOAD_FILES": "Select files to upload",
"UPLOAD_FILES_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team.",
"UPLOAD_FILES_NOT_ALLOWED": "Files cannot be uploaded whilst viewing the current items",
"UPLOAD_FOLDERS": "Select folders to upload",
"UPLOAD_FOLDERS_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team."
"UPLOAD_FOLDERS_NOT_ALLOWED": "Folders cannot be uploaded whilst viewing the current items"
}
},
"BROWSE": {

View File

@ -27,7 +27,7 @@
"openWith": [
{
"id": "plugin1.viewer.openWith.action1",
"type": "default",
"type": "button",
"icon": "build",
"title": "Snackbar",
"actions": {
@ -53,6 +53,52 @@
"content": {
"actions": [
{
"disabled": true,
"id": "app.toolbar.createFolder",
"type": "button",
"order": 10,
"title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
"icon": "create_new_folder",
"actions": {
"click": "CREATE_FOLDER"
},
"rules": {
"visible": "app.navigation.folder.canCreate"
}
},
{
"disabled": true,
"id": "app.toolbar.uploadFile",
"order": 11,
"type": "button",
"icon": "file_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES",
"actions": {
"click": "UPLOAD_FILES"
},
"rules": {
"visible": "app.navigation.folder.canUpload"
}
},
{
"disabled": true,
"id": "app.toolbar.uploadFolder",
"order": 12,
"type": "button",
"icon": "cloud_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS",
"actions": {
"click": "UPLOAD_FOLDER"
},
"rules": {
"visible": "app.navigation.folder.canUpload"
}
},
{
"disabled": true,
"id": "plugin1.toolbar.menu1",
"type": "menu",
"icon": "storage",
@ -70,6 +116,7 @@
]
},
{
"disabled": true,
"id": "plugin1.toolbar.separator3",
"order": 301,
"type": "separator"