diff --git a/e2e/components/menu/menu.ts b/e2e/components/menu/menu.ts index ba6796a8c..3e2cddf52 100755 --- a/e2e/components/menu/menu.ts +++ b/e2e/components/menu/menu.ts @@ -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); diff --git a/e2e/suites/actions/create-folder.test.ts b/e2e/suites/actions/create-folder.test.ts index ae0ce261b..355b5c4e0 100755 --- a/e2e/suites/actions/create-folder.test.ts +++ b/e2e/suites/actions/create-folder.test.ts @@ -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.`); }); }); diff --git a/extension.schema.json b/extension.schema.json index 1523515da..3d3b31f8e 100644 --- a/extension.schema.json +++ b/extension.schema.json @@ -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 } } }, diff --git a/src/app.config.json b/src/app.config.json index a83db09e1..a49196e18 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -9,7 +9,6 @@ "© 2017 - 2018 Alfresco Software, Inc. All rights reserved." }, "experimental": { - "libraries": false, "comments": false, "cardview": false, "permissions": false, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aa0953072..5c89f9ac5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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] }) diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 7730499f4..451045507 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -4,129 +4,17 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
-
+
diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts index da332aa3f..33da9d6d8 100644 --- a/src/app/components/favorites/favorites.component.ts +++ b/src/app/components/favorites/favorites.component.ts @@ -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()) ]); } diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index e163ac382..8601d3d44 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -7,125 +7,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
@@ -141,6 +25,7 @@ [disabled]="!canUpload"> -
+
diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts index a8f706818..0788d3f39 100644 --- a/src/app/components/files/files.component.spec.ts +++ b/src/app/components/files/files.component.spec.ts @@ -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; 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 = { id: '1' }; + component.node = { id: '1' }; - nodeActionsService.contentCopied.next(nodes); + nodeActionsService.contentCopied.next(nodes); expect(component.documentList.reload).toHaveBeenCalled(); }); @@ -183,9 +162,9 @@ describe('FilesComponent', () => { { entry: { parentId: '2' } } ]; - component.node = { id: '3' }; + component.node = { id: '3' }; - nodeActionsService.contentCopied.next(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 = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadComplete.next(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 = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadComplete.next(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 = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadDeleted.next(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 = { id: 'parentId' }; + const file: any = { file: { options: { parentId: 'otherId' } } }; + component.node = { id: 'parentId' }; - uploadService.fileUploadDeleted.next(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 = { 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', () => { - (component).node = { + component.node = { path: { elements: [ {id: 'node-id'} ] } diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index cce77e058..98078cd80 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -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 { - return this.contentApi.getNodeChildren(parentNodeId); - } - navigate(nodeId: string = null) { const commands = [ './' ]; diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index c140731b4..69cb63d40 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -4,39 +4,11 @@ - + - - - - - - - - + + + @@ -45,6 +17,7 @@
; node: MinimalNodeEntryEntity; selection: SelectionState; - displayMode = DisplayMode.List; + documentDisplayMode$: Observable; sharedPreviewUrl$: Observable; actions: Array = []; - canUpdateFile = false; + viewerActions: Array = []; 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; } } diff --git a/src/app/components/preview/preview.component.html b/src/app/components/preview/preview.component.html index f4500aa7a..fcb908865 100644 --- a/src/app/components/preview/preview.component.html +++ b/src/app/components/preview/preview.component.html @@ -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 @@ - + + + - - - - - - - - - - - - - - - - - + + diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index c007477f0..25cbe2b0e 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -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); } diff --git a/src/app/components/preview/preview.module.ts b/src/app/components/preview/preview.module.ts index d718a0bc4..f43f3bcfe 100644 --- a/src/app/components/preview/preview.module.ts +++ b/src/app/components/preview/preview.module.ts @@ -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, diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index f99f39e67..34090f592 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -4,116 +4,10 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
@@ -121,6 +15,7 @@
-
+
diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index ca29db79e..760ee310c 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -3,80 +3,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + +
@@ -154,7 +82,7 @@
-
+
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 1d9de2c10..45e047237 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -4,113 +4,10 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -118,6 +15,7 @@
-
+
diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 4a190d3c4..5ce534892 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -1,75 +1,39 @@
- - arrow_drop_down + + arrow_drop_down
queue
- - + + - - - - - - -
-
+
    -
  • + [attr.title]="item.description | translate"> + ` +}) +export class DocumentDisplayModeComponent { + + displayMode$: Observable; + + constructor(private store: Store) { + this.displayMode$ = store.select(documentDisplayMode); + } + + onClick() { + this.store.dispatch(new ToggleDocumentDisplayMode()); + } +} diff --git a/src/app/directives/edit-folder.directive.ts b/src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts similarity index 58% rename from src/app/directives/edit-folder.directive.ts rename to src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts index 650e2c3bc..a7e527967 100644 --- a/src/app/directives/edit-folder.directive.ts +++ b/src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts @@ -23,25 +23,30 @@ * along with Alfresco. If not, see . */ -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: ` + + ` }) -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; + + constructor(private store: Store) { + this.selection$ = this.store.select(appSelection); } - - constructor(private store: Store) {} } diff --git a/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts b/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts new file mode 100644 index 000000000..8e501a474 --- /dev/null +++ b/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts @@ -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 . + */ + +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: ` + + ` +}) +export class ToggleInfoDrawerComponent { + infoDrawerOpened$: Observable; + + constructor(private store: Store) { + this.infoDrawerOpened$ = this.store.select(infoDrawerOpened); + } + + onClick() { + this.store.dispatch(new ToggleInfoDrawerAction()); + } +} diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html index 0515ed64d..eb7a841b1 100644 --- a/src/app/components/trashcan/trashcan.component.html +++ b/src/app/components/trashcan/trashcan.component.html @@ -4,36 +4,10 @@ - + - - - - - - - - - - + +
@@ -41,6 +15,7 @@
{ + entry['isLibrary'] = this.isLibrary; + return entry; + }); + this.store.dispatch( - new SetSelectedNodesAction(this.documentList.selection) + new SetSelectedNodesAction(selection) ); } diff --git a/src/app/directives/node-copy.directive.spec.ts b/src/app/directives/node-copy.directive.spec.ts deleted file mode 100644 index 911c195ce..000000000 --- a/src/app/directives/node-copy.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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: '
' -}) -class TestComponent { - selection; -} - -describe('NodeCopyDirective', () => { - let fixture: ComponentFixture; - 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(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(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(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(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(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(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(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(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(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(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(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(contentApi.deleteNode).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - }); - }); - -}); diff --git a/src/app/directives/node-copy.directive.ts b/src/app/directives/node-copy.directive.ts deleted file mode 100644 index c8e994190..000000000 --- a/src/app/directives/node-copy.directive.ts +++ /dev/null @@ -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 . - */ - -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 - }); - } - ); - } -} diff --git a/src/app/directives/node-delete.directive.spec.ts b/src/app/directives/node-delete.directive.spec.ts deleted file mode 100644 index 8fe99c3d8..000000000 --- a/src/app/directives/node-delete.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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: '
' -}) -class TestComponent { - selection; -} - -describe('NodeDeleteDirective', () => { - let component: TestComponent; - let fixture: ComponentFixture; - 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(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(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(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(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(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(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(); - }); - }); - */ -}); diff --git a/src/app/directives/node-delete.directive.ts b/src/app/directives/node-delete.directive.ts deleted file mode 100644 index a5a70a61c..000000000 --- a/src/app/directives/node-delete.directive.ts +++ /dev/null @@ -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 . - */ - -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) {} - - @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)); - } - } -} diff --git a/src/app/directives/node-move.directive.spec.ts b/src/app/directives/node-move.directive.spec.ts deleted file mode 100644 index 9d0ae8002..000000000 --- a/src/app/directives/node-move.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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: '
' -}) -class TestComponent { - selection; -} - -describe('NodeMoveDirective', () => { - let fixture: ComponentFixture; - 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(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(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(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(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(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(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(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(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(movedItems); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(contentApi.restoreNode).toHaveBeenCalled(); - })); - }); - -}); diff --git a/src/app/directives/node-move.directive.ts b/src/app/directives/node-move.directive.ts deleted file mode 100644 index 09aa9105e..000000000 --- a/src/app/directives/node-move.directive.ts +++ /dev/null @@ -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 . - */ - -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, - 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)); - } - ); - } - -} diff --git a/src/app/directives/node-permanent-delete.directive.spec.ts b/src/app/directives/node-permanent-delete.directive.spec.ts deleted file mode 100644 index 6981144eb..000000000 --- a/src/app/directives/node-permanent-delete.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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: `
` -}) -class TestComponent { - selection = []; -} - -describe('NodePermanentDeleteDirective', () => { - let fixture: ComponentFixture; - 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(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(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(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(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(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(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(); - })); - }); -}); diff --git a/src/app/directives/node-permissions.directive.ts b/src/app/directives/node-permissions.directive.ts deleted file mode 100644 index 92973e104..000000000 --- a/src/app/directives/node-permissions.directive.ts +++ /dev/null @@ -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 . - */ - -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, - 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 || (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') - ); - } - } -} diff --git a/src/app/directives/node-restore.directive.spec.ts b/src/app/directives/node-restore.directive.spec.ts deleted file mode 100644 index 02d85912b..000000000 --- a/src/app/directives/node-restore.directive.spec.ts +++ /dev/null @@ -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 . - */ - -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: `
` -}) -class TestComponent { - selection: Array = []; -} - -describe('NodeRestoreDirective', () => { - let fixture: ComponentFixture; - 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(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(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(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(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(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(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(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(); - })); - }); -}); diff --git a/src/app/directives/node-unshare.directive.ts b/src/app/directives/node-unshare.directive.ts deleted file mode 100644 index 2426e393e..000000000 --- a/src/app/directives/node-unshare.directive.ts +++ /dev/null @@ -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 . - */ - -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(); - } -} diff --git a/src/app/directives/node-versions.directive.ts b/src/app/directives/node-versions.directive.ts deleted file mode 100644 index aceddc086..000000000 --- a/src/app/directives/node-versions.directive.ts +++ /dev/null @@ -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 . - */ - -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, - private contentApi: ContentApiService, - private dialog: MatDialog - ) {} - - async onManageVersions() { - if (this.node && this.node.entry) { - let entry = this.node.entry; - - if (entry.nodeId || (entry).guid) { - entry = await this.contentApi.getNodeInfo( - entry.nodeId || (entry).id - ).toPromise(); - this.openVersionManagerDialog(entry); - } else { - this.openVersionManagerDialog(entry); - } - } else if (this.node) { - this.openVersionManagerDialog(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') - ); - } - } -} diff --git a/src/app/extensions/action.extensions.ts b/src/app/extensions/action.extensions.ts index 7135fb22e..24f2ddb57 100644 --- a/src/app/extensions/action.extensions.ts +++ b/src/app/extensions/action.extensions.ts @@ -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; diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html deleted file mode 100644 index b69d1f8d4..000000000 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/extensions/components/toolbar/toolbar-action.component.html b/src/app/extensions/components/toolbar/toolbar-action.component.html new file mode 100644 index 000000000..9a6f36050 --- /dev/null +++ b/src/app/extensions/components/toolbar/toolbar-action.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts b/src/app/extensions/components/toolbar/toolbar-action.component.ts similarity index 66% rename from src/app/extensions/components/toolbar-action/toolbar-action.component.ts rename to src/app/extensions/components/toolbar/toolbar-action.component.ts index d3b18c549..c2d3c6483 100644 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts +++ b/src/app/extensions/components/toolbar/toolbar-action.component.ts @@ -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 = new Subject(); - constructor( protected store: Store, 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; } } diff --git a/src/app/extensions/components/toolbar/toolbar-button.component.ts b/src/app/extensions/components/toolbar/toolbar-button.component.ts new file mode 100644 index 000000000..122a4a3fe --- /dev/null +++ b/src/app/extensions/components/toolbar/toolbar-button.component.ts @@ -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 . + */ + +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: ` + + + + + + + + + ` +}) +export class ToolbarButtonComponent { + @Input() type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON; + @Input() actionRef: ContentActionRef; + + constructor( + protected store: Store, + private extensions: ExtensionService + ) {} + + runAction() { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + this.extensions.runActionById(this.actionRef.actions.click, { + selection + }); + }); + } +} diff --git a/src/app/extensions/core.extensions.module.ts b/src/app/extensions/core.extensions.module.ts index 197e841d6..37efa528c 100644 --- a/src/app/extensions/core.extensions.module.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -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 { diff --git a/src/app/extensions/evaluators/app.evaluators.ts b/src/app/extensions/evaluators/app.evaluators.ts index 79b5959ca..d14cae9e1 100644 --- a/src/app/extensions/evaluators/app.evaluators.ts +++ b/src/app/extensions/evaluators/app.evaluators.ts @@ -23,63 +23,201 @@ * along with Alfresco. If not, see . */ -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; } diff --git a/src/app/extensions/evaluators/navigation.evaluators.ts b/src/app/extensions/evaluators/navigation.evaluators.ts new file mode 100644 index 000000000..f6970f5a6 --- /dev/null +++ b/src/app/extensions/evaluators/navigation.evaluators.ts @@ -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 . + */ + +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); +} diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index b284a86d0..2ba72f9e3 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -41,6 +41,7 @@ export interface ExtensionConfig { create?: Array; viewer?: { openWith?: Array; + actions?: Array; }; navbar?: Array; content?: { diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 06b9a6d70..81f6772b8 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -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 = []; contentActions: Array = []; + viewerActions: Array = []; openWithActions: Array = []; createActions: Array = []; navbar: Array = []; @@ -63,7 +65,10 @@ export class ExtensionService implements RuleContext { selection: SelectionState; navigation: NavigationState; - constructor(private http: HttpClient, private store: Store) { + constructor( + private http: HttpClient, + private store: Store, + 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 { + 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]; diff --git a/src/app/extensions/components/custom-component/demo.button.ts b/src/app/extensions/permission.extensions.ts similarity index 81% rename from src/app/extensions/components/custom-component/demo.button.ts rename to src/app/extensions/permission.extensions.ts index 84fb9ddb3..fb399cbe5 100644 --- a/src/app/extensions/components/custom-component/demo.button.ts +++ b/src/app/extensions/permission.extensions.ts @@ -23,14 +23,6 @@ * along with Alfresco. If not, see . */ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-demo-button', - template: ` - - ` -}) -export class DemoButtonComponent {} +export interface NodePermissions { + check(source: any, permissions: string[], options?: any): boolean; +} diff --git a/src/app/extensions/rule.extensions.ts b/src/app/extensions/rule.extensions.ts index 37ffd4d49..bbab0c3e2 100644 --- a/src/app/extensions/rule.extensions.ts +++ b/src/app/extensions/rule.extensions.ts @@ -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 { diff --git a/src/app/services/content-api.service.ts b/src/app/services/content-api.service.ts index 03708d8bf..2adba098b 100644 --- a/src/app/services/content-api.service.ts +++ b/src/app/services/content-api.service.ts @@ -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): Observable { + 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-', payload)); + } + + removeFavorite(nodes: Array): Observable { + return Observable.from(Promise.all(nodes.map(node => { + const id = node.entry.nodeId || node.entry.id; + return this.api.favoritesApi.removeFavoriteSite('-me-', id); + }))); + } } diff --git a/src/app/services/content-management.service.spec.ts b/src/app/services/content-management.service.spec.ts new file mode 100644 index 000000000..a9687b2be --- /dev/null +++ b/src/app/services/content-management.service.spec.ts @@ -0,0 +1,1275 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + + +import { TestBed, fakeAsync } from '@angular/core/testing'; +import { Observable } from 'rxjs/Rx'; +import { MatDialog, MatSnackBar } from '@angular/material'; +import { Actions, ofType, EffectsModule } from '@ngrx/effects'; +import { + SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction, + SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING, PurgeDeletedNodesAction, + RestoreDeletedNodesAction, NavigateRouteAction, NAVIGATE_ROUTE, DeleteNodesAction, MoveNodesAction, CopyNodesAction +} 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'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../store/states'; +import { ContentManagementService } from './content-management.service'; +import { NodeActionsService } from './node-actions.service'; +import { TranslationService } from '@alfresco/adf-core'; + +describe('ContentManagementService', () => { + + let dialog: MatDialog; + let actions$: Actions; + let contentApi: ContentApiService; + let store: Store; + let contentManagementService: ContentManagementService; + let snackBar: MatSnackBar; + let nodeActions: NodeActionsService; + let translationService: TranslationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AppTestingModule, + EffectsModule.forRoot([NodeEffects]) + ] + }); + + contentApi = TestBed.get(ContentApiService); + actions$ = TestBed.get(Actions); + store = TestBed.get(Store); + contentManagementService = TestBed.get(ContentManagementService); + snackBar = TestBed.get(MatSnackBar); + nodeActions = TestBed.get(NodeActionsService); + translationService = TestBed.get(TranslationService); + + dialog = TestBed.get(MatDialog); + spyOn(dialog, 'open').and.returnValue({ + afterClosed() { + return Observable.of(true); + } + }); + }); + + describe('Copy node action', () => { + beforeEach(() => { + spyOn(snackBar, 'open').and.callThrough(); + }); + + it('notifies successful copy of a node', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + + it('notifies successful copy of multiple nodes', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const 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' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const 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' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const 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' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const 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 = []; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy', name: 'name' } }]; + const createdItems = []; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.of('')); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies permission error on copy of node', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new CopyNodesAction(selection)); + + expect(nodeActions.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(nodeActions, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + }); + + describe('Undo Copy action', () => { + beforeEach(() => { + spyOn(nodeActions, '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)); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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)); + + const 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' } }] ]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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)); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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!'))); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.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}})))); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(contentApi.deleteNode).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + }); + + describe('Move node action', () => { + beforeEach(() => { + spyOn(translationService, 'instant').and.callFake((keysArray) => { + if (Array.isArray(keysArray)) { + const processedKeys = {}; + keysArray.forEach((key) => { + processedKeys[key] = key; + }); + return processedKeys; + } else { + return keysArray; + } + }); + }); + + 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(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = node; + store.dispatch(new MoveNodesAction(selection)); + + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.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(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL'); + }); + + it('notifies partial move of a node', () => { + const nodes = [ { entry: { id: '1', name: 'name' } } ]; + const moveResponse = { + succeeded: [], + failed: [], + partiallySucceeded: nodes + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.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(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.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(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.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(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.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 nodes = [{ entry: { id: 'node-to-move-id', name: 'name' } }]; + const moveResponse = { + succeeded: [], + failed: [], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('')); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies permission error on move of node', () => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.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(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies conflict error message on 409', () => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.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 nodes = [ { entry: { id: '1', name: 'name' } } ]; + const moveResponse = { + succeeded: [], + failed: [ {} ], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + }); + + describe('Undo Move action', () => { + beforeEach(() => { + spyOn(translationService, 'instant').and.callFake((keysArray) => { + if (Array.isArray(keysArray)) { + const processedKeys = {}; + keysArray.forEach((key) => { + processedKeys[key] = key; + }); + return processedKeys; + } else { + return keysArray; + } + }); + }); + + beforeEach(() => { + spyOn(nodeActions, '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 } }; + const selection = [ node ]; + + spyOn(nodeActions, 'moveNodeAction').and.returnValue(Observable.of({})); + + store.dispatch(new MoveNodesAction(selection)); + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ { itemMoved: node, initialParentId: initialParent} ] + }; + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.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 } }; + const selection = [ node ]; + + spyOn(nodeActions, 'moveNodeAction').and.returnValue(Observable.of({})); + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ node ] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.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 } }; + const selection = [ node ]; + + const itemMoved = {}; // folder was empty + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ [ itemMoved ] ] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(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(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'conflicting-name', parentId: initialParent } }; + const selection = [node]; + + const afterMoveParentId = 'parent-id-1'; + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name', parentId: afterMoveParentId } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(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(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; + const selection = [ node ]; + + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(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(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; + const selection = [ node ]; + + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(contentApi.restoreNode).toHaveBeenCalled(); + })); + }); + + 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(SNACKBAR_INFO), + map(action => { + done(); + }) + ); + + const selection = [{ entry: { id: '1', name: 'name1' } }]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise error message on failed single file deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); + + const selection = [{ entry: { id: '1', name: 'name1' } }]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise info message on successful multiple files deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise error message failed multiple files deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + 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(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + 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(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + }); + + + describe('Permanent Delete', () => { + it('does not purge nodes if no selection', () => { + spyOn(contentApi, 'purgeDeletedNode'); + + store.dispatch(new PurgeDeletedNodesAction([])); + expect(contentApi.purgeDeletedNode).not.toHaveBeenCalled(); + }); + + it('call purge nodes if selection is not empty', fakeAsync(() => { + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); + + const selection = [ { entry: { id: '1' } } ]; + store.dispatch(new PurgeDeletedNodesAction(selection)); + + expect(contentApi.purgeDeletedNode).toHaveBeenCalled(); + })); + + describe('notification', () => { + it('raises warning on multiple fail and one success', fakeAsync(done => { + actions$.pipe( + ofType(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({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises warning on multiple success and multiple fail', fakeAsync(done => { + actions$.pipe( + ofType(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({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } }, + { entry: { id: '4', name: 'name4' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises info on one selected node success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); + + const selection = [ + { entry: { id: '1', name: 'name1' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises error on one selected node fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.throw({})); + + const selection = [ + { entry: { id: '1', name: 'name1' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises info on all nodes success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.of({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises error on all nodes fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.throw({}); + } + + if (id === '2') { + return Observable.throw({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + }); + }); + + describe('Restore Deleted', () => { + it('does not restore nodes if no selection', () => { + spyOn(contentApi, 'restoreNode'); + + const selection = []; + store.dispatch(new RestoreDeletedNodesAction(selection)); + + expect(contentApi.restoreNode).not.toHaveBeenCalled(); + }); + + it('does not restore nodes if selection has nodes without path', () => { + spyOn(contentApi, 'restoreNode'); + + const selection = [ { entry: { id: '1' } } ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + 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' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + 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' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + 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(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' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } }, + { entry: { id: '2', name: 'name2', path } }, + { entry: { id: '3', name: 'name3', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + 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(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + 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(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + 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(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + 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(SNACKBAR_INFO), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } }, + { entry: { id: '2', name: 'name2', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + xit('should raise info message when restore selected node', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('navigate to restore selected node location onAction', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + + actions$.pipe( + ofType(NAVIGATE_ROUTE), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + name: 'name1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + }); + }); + +}); diff --git a/src/app/services/content-management.service.ts b/src/app/services/content-management.service.ts index fd5e38fed..74f903081 100644 --- a/src/app/services/content-management.service.ts +++ b/src/app/services/content-management.service.ts @@ -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(); libraryCreated = new Subject(); linksUnshared = new Subject(); + favoriteAdded = new Subject>(); + favoriteRemoved = new Subject>(); constructor( private store: Store, private contentApi: ContentApiService, private permission: NodePermissionService, - private dialogRef: MatDialog + private dialogRef: MatDialog, + private nodeActionsService: NodeActionsService, + private translation: TranslationService, + private snackBar: MatSnackBar ) {} + addFavorite(nodes: Array) { + 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) { + 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) { + 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) { + 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, + newItems?: Array + ) { + 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) { + 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[] = []; + + 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[] = []; + + 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 { + 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 { const { entry } = node; @@ -457,4 +831,170 @@ export class ContentManagementService { } }); } + + private deleteNode(node: MinimalNodeEntity): Observable { + 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, 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; + } } diff --git a/src/app/services/node-permission.service.ts b/src/app/services/node-permission.service.ts index 3ef8ad765..fa069834c 100644 --- a/src/app/services/node-permission.service.ts +++ b/src/app/services/node-permission.service.ts @@ -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) { diff --git a/src/app/store/actions.ts b/src/app/store/actions.ts index 013545cb3..cacbc9c29 100644 --- a/src/app/store/actions.ts +++ b/src/app/store/actions.ts @@ -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'; diff --git a/src/app/store/actions/app.actions.ts b/src/app/store/actions/app.actions.ts index 33662ba14..81b3b9c2a 100644 --- a/src/app/store/actions/app.actions.ts +++ b/src/app/store/actions/app.actions.ts @@ -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) {} +} diff --git a/src/app/directives/node-restore.directive.ts b/src/app/store/actions/favorite.actions.ts similarity index 67% rename from src/app/directives/node-restore.directive.ts rename to src/app/store/actions/favorite.actions.ts index 4380d6c34..c4f466166 100644 --- a/src/app/directives/node-restore.directive.ts +++ b/src/app/store/actions/favorite.actions.ts @@ -23,23 +23,18 @@ * along with Alfresco. If not, see . */ -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) {} - - @HostListener('click') - onClick() { - this.store.dispatch(new RestoreDeletedNodesAction(this.selection)); - } +export class AddFavoriteAction implements Action { + readonly type = ADD_FAVORITE; + constructor(public payload: Array) {} +} + +export class RemoveFavoriteAction implements Action { + readonly type = REMOVE_FAVORITE; + constructor(public payload: Array) {} } diff --git a/src/app/store/actions/library.actions.ts b/src/app/store/actions/library.actions.ts index bc6540028..1613286c8 100644 --- a/src/app/store/actions/library.actions.ts +++ b/src/app/store/actions/library.actions.ts @@ -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 { diff --git a/src/app/store/actions/node.actions.ts b/src/app/store/actions/node.actions.ts index f3e06ee11..5173c1158 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -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) {} +} + +export class CopyNodesAction implements Action { + readonly type = COPY_NODES; + constructor(public payload: Array) {} +} + +export class MoveNodesAction implements Action { + readonly type = MOVE_NODES; + constructor(public payload: Array) {} +} + +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) {} +} diff --git a/src/app/directives/node-permanent-delete.directive.ts b/src/app/store/actions/upload.actions.ts similarity index 62% rename from src/app/directives/node-permanent-delete.directive.ts rename to src/app/store/actions/upload.actions.ts index b32b5e895..905cb5712 100644 --- a/src/app/directives/node-permanent-delete.directive.ts +++ b/src/app/store/actions/upload.actions.ts @@ -23,27 +23,17 @@ * along with Alfresco. If not, see . */ -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 - ) {} - - @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) {} } diff --git a/src/app/store/app-store.module.ts b/src/app/store/app-store.module.ts index ecf31fd66..7c66142db 100644 --- a/src/app/store/app-store.module.ts +++ b/src/app/store/app-store.module.ts @@ -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 }) diff --git a/src/app/store/effects.ts b/src/app/store/effects.ts index 3c773fee2..b3e84d075 100644 --- a/src/app/store/effects.ts +++ b/src/app/store/effects.ts @@ -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'; diff --git a/src/app/store/effects/favorite.effects.ts b/src/app/store/effects/favorite.effects.ts new file mode 100644 index 000000000..43aff146c --- /dev/null +++ b/src/app/store/effects/favorite.effects.ts @@ -0,0 +1,82 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { 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, + private actions$: Actions, + private content: ContentManagementService + ) {} + + @Effect({ dispatch: false }) + addFavorite$ = this.actions$.pipe( + ofType(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(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); + } + }); + } + }) + ); + + +} diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index d9debf63e..1efe5c260 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -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, - private contentApi: ContentApiService, + private actions$: Actions, private content: ContentManagementService ) {} @@ -53,7 +48,16 @@ export class SiteEffects { ofType(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(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(); - } } diff --git a/src/app/store/effects/node.effects.ts b/src/app/store/effects/node.effects.ts index 95794d3c2..5a43ffd00 100644 --- a/src/app/store/effects/node.effects.ts +++ b/src/app/store/effects/node.effects.ts @@ -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, private actions$: Actions, - private contentManagementService: ContentManagementService, - private contentApi: ContentApiService + private contentService: ContentManagementService ) {} + @Effect({ dispatch: false }) + shareNode$ = this.actions$.pipe( + ofType(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(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(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(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(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(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(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(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[] = []; - - items.forEach(node => { - batch.push(this.deleteNode(node)); - }); - - Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => { - const status = this.processStatus(data); - const message = this.getDeleteMessage(status); - - if (message && status.someSucceeded) { - message.duration = 10000; - message.userAction = new SnackbarUserAction( - 'APP.ACTIONS.UNDO', - new UndoDeleteNodesAction([...status.success]) - ); - } - - this.store.dispatch(message); - - if (status.someSucceeded) { - this.contentManagementService.nodesDeleted.next(); - } - }); - } - - private deleteNode(node: NodeInfo): Observable { - const { id, name } = node; - - return 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[] = []; - - items.forEach(item => { - batch.push(this.undoDeleteNode(item)); - }); - - Observable.forkJoin(...batch).subscribe(data => { - const processedData = this.processStatus(data); - - if (processedData.fail.length) { - const message = this.getUndoDeleteMessage(processedData); - this.store.dispatch(message); - } - - if (processedData.someSucceeded) { - this.contentManagementService.nodesRestored.next(); - } - }); - } - - private undoDeleteNode(item: DeletedNodeInfo): Observable { - const { id, name } = item; - - return 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(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(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(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(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 + ); + } + }); + } + }) + ); } diff --git a/src/app/store/effects/upload.effects.ts b/src/app/store/effects/upload.effects.ts new file mode 100644 index 000000000..0f525b5a5 --- /dev/null +++ b/src/app/store/effects/upload.effects.ts @@ -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 . + */ + +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, + 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(UPLOAD_FILES), + map(() => { + this.fileInput.click(); + }) + ); + + @Effect({ dispatch: false }) + uploadFolder$ = this.actions$.pipe( + ofType(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 = 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(); + }); + } + } +} diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts index 2d789c5a9..864b53eb4 100644 --- a/src/app/store/reducers/app.reducer.ts +++ b/src/app/store/reducers/app.reducer.ts @@ -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, action); break; + case TOGGLE_INFO_DRAWER: + newState = updateInfoDrawer(state, 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; } diff --git a/src/app/store/selectors/app.selectors.ts b/src/app/store/selectors/app.selectors.ts index 4e5f8788c..dc2af14fd 100644 --- a/src/app/store/selectors/app.selectors.ts +++ b/src/app/store/selectors/app.selectors.ts @@ -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, diff --git a/src/app/store/states/app.state.ts b/src/app/store/states/app.state.ts index 2b134d029..4002a9291 100644 --- a/src/app/store/states/app.state.ts +++ b/src/app/store/states/app.state.ts @@ -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 { diff --git a/src/app/store/states/selection.state.ts b/src/app/store/states/selection.state.ts index 9db206192..b822baae1 100644 --- a/src/app/store/states/selection.state.ts +++ b/src/app/store/states/selection.state.ts @@ -23,14 +23,16 @@ * along with Alfresco. If not, see . */ -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; } diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 44a1a63b5..7e408273a 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -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" + } } ] } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7eb471321..adacab4e3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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": { diff --git a/src/assets/plugins/plugin1.json b/src/assets/plugins/plugin1.json index 5d7cc4301..a3587600c 100644 --- a/src/assets/plugins/plugin1.json +++ b/src/assets/plugins/plugin1.json @@ -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"