From e004d365a9aaef2b0e023eeea6784bfec5587b94 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 1 Feb 2019 13:52:08 +0200 Subject: [PATCH] [ACA-213] Edit Offline (#909) * WRITE_LOCK evaluator * evaluate actions for WRITE_LOCK * edit offline action * DL icon for WRITE_LOCK files * edit offline directive * custom name column * localisation * toggle offline edit extension * move takeUntil operator * add tooltip * better selector to differentiate Edit folder from Edit Offline * default to empty object for null properties object * isPersonalFiles evaluator * isLibraryFiles evaluator * isLibraryFiles evaluator * isPersonalFiles evaluator * update canEditLockedFile rule --- e2e/components/menu/menu.ts | 6 +- src/app/app.module.ts | 2 + .../document-list-custom-components.module.ts | 39 +++++ .../locked-by/locked-by.component.scss | 11 ++ .../locked-by/locked-by.component.ts | 61 ++++++++ .../name-column/name-column.component.scss | 17 +++ .../name-column/name-column.component.spec.ts | 106 +++++++++++++ .../name-column/name-column.component.ts | 94 ++++++++++++ src/app/components/page.component.ts | 9 +- .../toggle-edit-offline.component.spec.ts | 132 ++++++++++++++++ .../toggle-edit-offline.component.ts | 91 +++++++++++ src/app/components/toolbar/toolbar.module.ts | 4 +- src/app/directives/directives.module.ts | 4 +- .../directives/edit-offline.directive.spec.ts | 141 ++++++++++++++++++ src/app/directives/edit-offline.directive.ts | 105 +++++++++++++ src/app/extensions/core.extensions.module.ts | 13 +- .../extensions/evaluators/app.evaluators.ts | 25 ++++ .../evaluators/navigation.evaluators.ts | 16 ++ src/app/store/actions/node.actions.ts | 6 + src/assets/app.extensions.json | 130 +++++++++++----- src/assets/i18n/en.json | 8 +- 21 files changed, 971 insertions(+), 49 deletions(-) create mode 100644 src/app/components/dl-custom-components/document-list-custom-components.module.ts create mode 100644 src/app/components/dl-custom-components/locked-by/locked-by.component.scss create mode 100644 src/app/components/dl-custom-components/locked-by/locked-by.component.ts create mode 100644 src/app/components/dl-custom-components/name-column/name-column.component.scss create mode 100644 src/app/components/dl-custom-components/name-column/name-column.component.spec.ts create mode 100644 src/app/components/dl-custom-components/name-column/name-column.component.ts create mode 100644 src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.spec.ts create mode 100644 src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.ts create mode 100644 src/app/directives/edit-offline.directive.spec.ts create mode 100644 src/app/directives/edit-offline.directive.ts diff --git a/e2e/components/menu/menu.ts b/e2e/components/menu/menu.ts index 0a5a2f346..ed0ed7bf3 100755 --- a/e2e/components/menu/menu.ts +++ b/e2e/components/menu/menu.ts @@ -35,7 +35,9 @@ export class Menu extends Component { icon: '.mat-icon', uploadFiles: 'app-upload-files', - submenu: 'app-context-menu-item .mat-menu-item' + submenu: 'app-context-menu-item .mat-menu-item', + + editFolder: `app.context.menu.editFolder` }; items: ElementArrayFinder = this.component.all(by.css(Menu.selectors.item)); @@ -47,7 +49,7 @@ export class Menu extends Component { shareEditAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Shared link settings')); viewAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'View')); downloadAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Download')); - editAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Edit')); + editAction: ElementFinder = this.component.element(by.id(Menu.selectors.editFolder)); copyAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Copy')); moveAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Move')); deleteAction: ElementFinder = this.component.element(by.cssContainingText(Menu.selectors.item, 'Delete')); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 98391279c..529759f53 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -70,6 +70,7 @@ import { AppCommonModule } from './components/common/common.module'; import { AppLayoutModule } from './components/layout/layout.module'; import { AppCurrentUserModule } from './components/current-user/current-user.module'; import { AppSearchInputModule } from './components/search/search-input.module'; +import { DocumentListCustomComponentsModule } from './components/dl-custom-components/document-list-custom-components.module'; import { AppSearchResultsModule } from './components/search/search-results.module'; import { AppLoginModule } from './components/login/login.module'; import { AppHeaderModule } from './components/header/header.module'; @@ -105,6 +106,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; AppSharedModule, AppSidenavModule, AppCreateMenuModule, + DocumentListCustomComponentsModule, AppPermissionsModule, AppSearchInputModule, AppSearchResultsModule, diff --git a/src/app/components/dl-custom-components/document-list-custom-components.module.ts b/src/app/components/dl-custom-components/document-list-custom-components.module.ts new file mode 100644 index 000000000..f0b149fcc --- /dev/null +++ b/src/app/components/dl-custom-components/document-list-custom-components.module.ts @@ -0,0 +1,39 @@ +/*! + * @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 { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { CustomNameColumnComponent } from './name-column/name-column.component'; +import { LockByComponent } from './locked-by/locked-by.component'; +import { ContentModule } from '@alfresco/adf-content-services'; +import { MaterialModule } from '../../material.module'; + +@NgModule({ + imports: [BrowserModule, ContentModule, MaterialModule], + declarations: [CustomNameColumnComponent, LockByComponent], + exports: [CustomNameColumnComponent, LockByComponent], + entryComponents: [CustomNameColumnComponent, LockByComponent] +}) +export class DocumentListCustomComponentsModule {} diff --git a/src/app/components/dl-custom-components/locked-by/locked-by.component.scss b/src/app/components/dl-custom-components/locked-by/locked-by.component.scss new file mode 100644 index 000000000..b892ccfab --- /dev/null +++ b/src/app/components/dl-custom-components/locked-by/locked-by.component.scss @@ -0,0 +1,11 @@ +.aca-locked-by { + .locked_by--icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + .locked_by--name { + font-size: 12px; + } +} diff --git a/src/app/components/dl-custom-components/locked-by/locked-by.component.ts b/src/app/components/dl-custom-components/locked-by/locked-by.component.ts new file mode 100644 index 000000000..a64288485 --- /dev/null +++ b/src/app/components/dl-custom-components/locked-by/locked-by.component.ts @@ -0,0 +1,61 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + Input, + OnInit, + ChangeDetectionStrategy, + ViewEncapsulation +} from '@angular/core'; + +import { NodeEntry } from '@alfresco/js-api'; + +@Component({ + selector: 'aca-locked-by', + template: ` + lock + {{ writeLockedBy() }} + `, + styleUrls: ['./locked-by.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + class: 'aca-locked-by' + } +}) +export class LockByComponent implements OnInit { + @Input() + context: any; + + node: NodeEntry; + + constructor() {} + + ngOnInit() { + this.node = this.context.row.node; + } + + writeLockedBy() { + return ( + this.node && + this.node.entry.properties && + this.node.entry.properties['cm:lockOwner'] && + this.node.entry.properties['cm:lockOwner'].displayName + ); + } +} diff --git a/src/app/components/dl-custom-components/name-column/name-column.component.scss b/src/app/components/dl-custom-components/name-column/name-column.component.scss new file mode 100644 index 000000000..349f76c53 --- /dev/null +++ b/src/app/components/dl-custom-components/name-column/name-column.component.scss @@ -0,0 +1,17 @@ +.aca-name-column-container { + .adf-datatable-cell { + top: 8px; + } + + aca-locked-by { + position: absolute; + bottom: 6px; + display: flex; + align-items: center; + } +} + +.aca-custom-name-column { + display: flex; + align-items: center; +} diff --git a/src/app/components/dl-custom-components/name-column/name-column.component.spec.ts b/src/app/components/dl-custom-components/name-column/name-column.component.spec.ts new file mode 100644 index 000000000..31b1ef498 --- /dev/null +++ b/src/app/components/dl-custom-components/name-column/name-column.component.spec.ts @@ -0,0 +1,106 @@ +/*! + * @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 { CustomNameColumnComponent } from './name-column.component'; +import { DocumentListCustomComponentsModule } from '../document-list-custom-components.module'; +import { Actions } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; +import { TestBed } from '@angular/core/testing'; + +describe('CustomNameColumnComponent', () => { + let fixture; + let component; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + DocumentListCustomComponentsModule, + StoreModule.forRoot({ app: () => {} }, { initialState: {} }) + ], + providers: [Actions] + }); + + fixture = TestBed.createComponent(CustomNameColumnComponent); + component = fixture.componentInstance; + }); + + it('should not render lock element if file is not locked', () => { + component.context = { + row: { + node: { + entry: { + isFile: true, + id: 'nodeId' + } + } + } + }; + + fixture.detectChanges(); + + expect( + fixture.debugElement.nativeElement.querySelector('aca-locked-by') + ).toBe(null); + }); + + it('should not render lock element if node is not a file', () => { + component.context = { + row: { + node: { + entry: { + isFile: false, + id: 'nodeId' + } + } + } + }; + + fixture.detectChanges(); + + expect( + fixture.debugElement.nativeElement.querySelector('aca-locked-by') + ).toBe(null); + }); + + it('should render lock element if file is locked', () => { + component.context = { + row: { + node: { + entry: { + isFile: true, + id: 'nodeId', + properties: { 'cm:lockType': 'WRITE_LOCK' } + } + } + } + }; + + fixture.detectChanges(); + + expect( + fixture.debugElement.nativeElement.querySelector('aca-locked-by') + ).not.toBe(null); + }); +}); diff --git a/src/app/components/dl-custom-components/name-column/name-column.component.ts b/src/app/components/dl-custom-components/name-column/name-column.component.ts new file mode 100644 index 000000000..138c85de2 --- /dev/null +++ b/src/app/components/dl-custom-components/name-column/name-column.component.ts @@ -0,0 +1,94 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + Input, + OnInit, + ViewEncapsulation, + ChangeDetectorRef, + OnDestroy +} from '@angular/core'; +import { Actions, ofType } from '@ngrx/effects'; +import { EDIT_OFFLINE } from '../../../store/actions'; +import { NodeEntry } from '@alfresco/js-api'; +import { Subject } from 'rxjs'; +import { filter, takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'aca-custom-name-column', + template: ` +
+ + + + + +
+ `, + styleUrls: ['name-column.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CustomNameColumnComponent implements OnInit, OnDestroy { + node: NodeEntry; + + @Input() + context: any; + + private onDestroy$: Subject = new Subject(); + + constructor(private cd: ChangeDetectorRef, private actions$: Actions) {} + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + ngOnInit() { + this.node = this.context.row.node; + + this.actions$ + .pipe( + ofType(EDIT_OFFLINE), + filter(val => { + return this.node.entry.id === val.payload.entry.id; + }), + takeUntil(this.onDestroy$) + ) + .subscribe(() => { + this.cd.detectChanges(); + }); + } + + isFile() { + return this.node && this.node.entry && this.node.entry.isFile; + } + + isFileWriteLocked() { + return !!( + this.node && + this.node.entry && + this.node.entry.properties && + this.node.entry.properties['cm:lockType'] === 'WRITE_LOCK' + ); + } +} diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 0e19958fe..54976879e 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -71,6 +71,10 @@ export abstract class PageComponent implements OnInit, OnDestroy { ); } + static isWriteLockedNode(node) { + return node.properties && node.properties['cm:lockType'] === 'WRITE_LOCK'; + } + static isLibrary(entry) { return ( (entry.guid && @@ -135,7 +139,10 @@ export abstract class PageComponent implements OnInit, OnDestroy { imageResolver(row: ShareDataRow): string | null { const entry: MinimalNodeEntryEntity = row.node.entry; - if (PageComponent.isLockedNode(entry)) { + if ( + PageComponent.isLockedNode(entry) || + PageComponent.isWriteLockedNode(entry) + ) { return 'assets/images/ic_lock_black_24dp_1x.png'; } diff --git a/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.spec.ts b/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.spec.ts new file mode 100644 index 000000000..85ed8d52c --- /dev/null +++ b/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.spec.ts @@ -0,0 +1,132 @@ +/*! + * @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 { ToggleEditOfflineComponent } from './toggle-edit-offline.component'; +import { EditOfflineDirective } from '../../../directives/edit-offline.directive'; +import { setupTestBed, CoreModule } from '@alfresco/adf-core'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { + DownloadNodesAction, + EditOfflineAction, + SnackbarErrorAction +} from '../../../store/actions'; + +describe('ToggleEditOfflineComponent', () => { + let fixture; + let component; + let selection; + let store; + let dispatchSpy; + + setupTestBed({ + imports: [CoreModule], + declarations: [ToggleEditOfflineComponent, EditOfflineDirective], + providers: [ + { + provide: Store, + useValue: { + select: () => of(selection), + dispatch: () => {} + } + } + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ToggleEditOfflineComponent); + component = fixture.componentInstance; + store = TestBed.get(Store); + + dispatchSpy = spyOn(store, 'dispatch'); + }); + + afterEach(() => { + dispatchSpy.calls.reset(); + }); + + it('should initialized with data from store', () => { + selection = { file: { entry: { properties: {} } } }; + + fixture.detectChanges(); + + expect(component.selection).toEqual(selection.file); + }); + + it('should download content if node is locked', () => { + component.selection = { entry: { properties: {} } }; + + const isLocked = true; + component.onToggleEvent(isLocked); + + fixture.detectChanges(); + + expect(dispatchSpy.calls.argsFor(0)).toEqual([ + new DownloadNodesAction([component.selection]) + ]); + }); + + it('should not download content if node is not locked', () => { + component.selection = { entry: { properties: {} } }; + + const isLocked = false; + component.onToggleEvent(isLocked); + + fixture.detectChanges(); + + expect(dispatchSpy.calls.argsFor(0)).not.toEqual([ + new DownloadNodesAction([component.selection]) + ]); + }); + + it('should dispatch EditOfflineAction action', () => { + component.selection = { entry: { properties: {} } }; + + const isLocked = false; + component.onToggleEvent(isLocked); + + fixture.detectChanges(); + + expect(dispatchSpy.calls.argsFor(0)).toEqual([ + new EditOfflineAction(component.selection) + ]); + }); + + it('should raise notification on error', () => { + component.selection = { + entry: { name: 'test' } + }; + + component.onError(); + fixture.detectChanges(); + + expect(dispatchSpy.calls.argsFor(0)).toEqual([ + new SnackbarErrorAction('APP.MESSAGES.ERRORS.LOCK_NODE', { + fileName: 'test' + }) + ]); + }); +}); diff --git a/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.ts b/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.ts new file mode 100644 index 000000000..2a1f634f9 --- /dev/null +++ b/src/app/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.ts @@ -0,0 +1,91 @@ +/*! + * @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, ViewEncapsulation, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../../store/states'; +import { appSelection } from '../../../store/selectors/app.selectors'; +import { + DownloadNodesAction, + EditOfflineAction, + SnackbarErrorAction +} from '../../../store/actions'; +import { MinimalNodeEntity } from '@alfresco/js-api'; + +@Component({ + selector: 'app-toggle-edit-offline', + template: ` + + `, + encapsulation: ViewEncapsulation.None, + host: { class: 'app-toggle-edit-offline' } +}) +export class ToggleEditOfflineComponent implements OnInit { + selection: MinimalNodeEntity; + + constructor(private store: Store) {} + + ngOnInit() { + this.store.select(appSelection).subscribe(({ file }) => { + this.selection = file; + }); + } + + onToggleEvent(isNodeLocked: boolean) { + if (isNodeLocked) { + this.store.dispatch(new DownloadNodesAction([this.selection])); + } + this.store.dispatch(new EditOfflineAction(this.selection)); + } + + onError() { + this.store.dispatch( + new SnackbarErrorAction('APP.MESSAGES.ERRORS.LOCK_NODE', { + fileName: this.selection.entry.name + }) + ); + } +} diff --git a/src/app/components/toolbar/toolbar.module.ts b/src/app/components/toolbar/toolbar.module.ts index 4bbd9f295..aae49ac54 100644 --- a/src/app/components/toolbar/toolbar.module.ts +++ b/src/app/components/toolbar/toolbar.module.ts @@ -38,6 +38,7 @@ import { ToggleJoinLibraryButtonComponent } from './toggle-join-library/toggle-j import { ToggleJoinLibraryMenuComponent } from './toggle-join-library/toggle-join-library-menu.component'; import { DirectivesModule } from '../../directives/directives.module'; import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library/toggle-favorite-library.component'; +import { ToggleEditOfflineComponent } from './toggle-edit-offline/toggle-edit-offline.component'; import { AppCommonModule } from '../common/common.module'; export function components() { @@ -51,7 +52,8 @@ export function components() { ToolbarMenuComponent, ToggleJoinLibraryButtonComponent, ToggleJoinLibraryMenuComponent, - ToggleFavoriteLibraryComponent + ToggleFavoriteLibraryComponent, + ToggleEditOfflineComponent ]; } diff --git a/src/app/directives/directives.module.ts b/src/app/directives/directives.module.ts index f293dfdcc..17abca047 100644 --- a/src/app/directives/directives.module.ts +++ b/src/app/directives/directives.module.ts @@ -29,6 +29,7 @@ import { DocumentListDirective } from './document-list.directive'; import { PaginationDirective } from './pagination.directive'; import { LibraryMembershipDirective } from './library-membership.directive'; import { LibraryFavoriteDirective } from './library-favorite.directive'; +import { EditOfflineDirective } from './edit-offline.directive'; export function directives() { return [ @@ -36,7 +37,8 @@ export function directives() { DocumentListDirective, PaginationDirective, LibraryMembershipDirective, - LibraryFavoriteDirective + LibraryFavoriteDirective, + EditOfflineDirective ]; } diff --git a/src/app/directives/edit-offline.directive.spec.ts b/src/app/directives/edit-offline.directive.spec.ts new file mode 100644 index 000000000..ff772b5a3 --- /dev/null +++ b/src/app/directives/edit-offline.directive.spec.ts @@ -0,0 +1,141 @@ +/*! + * @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, ViewChild } from '@angular/core'; +import { EditOfflineDirective } from './edit-offline.directive'; +import { + AlfrescoApiService, + AlfrescoApiServiceMock, + setupTestBed, + CoreModule +} from '@alfresco/adf-core'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; + +@Component({ + selector: 'app-test-component', + template: ` + + ` +}) +class TestComponent { + @ViewChild('editOffline') + directive: EditOfflineDirective; + + selection = null; +} + +describe('EditOfflineDirective', () => { + let fixture; + let api; + let component; + + setupTestBed({ + imports: [CoreModule], + declarations: [TestComponent, EditOfflineDirective], + providers: [ + { + provide: AlfrescoApiService, + useClass: AlfrescoApiServiceMock + } + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + component.selection = null; + api = TestBed.get(AlfrescoApiService); + }); + + it('should return false if selection is not locked', () => { + component.selection = { entry: { name: 'test-name', properties: {} } }; + fixture.detectChanges(); + expect(component.directive.isNodeLocked()).toBe(false); + }); + + it('should return true if selection is locked', () => { + component.selection = { + entry: { + name: 'test-name', + properties: { 'cm:lockType': 'WRITE_LOCK' } + } + }; + + fixture.detectChanges(); + expect(component.directive.isNodeLocked()).toBe(true); + }); + + it('should lock selection', fakeAsync(() => { + component.selection = { + entry: { + id: 'id', + name: 'test-name', + properties: {} + } + }; + + spyOn(api.nodesApi, 'lockNode').and.returnValue( + Promise.resolve({ + entry: { properties: { 'cm:lockType': 'WRITE_LOCK' } } + }) + ); + + fixture.detectChanges(); + + component.directive.onClick(); + tick(); + fixture.detectChanges(); + + expect(component.selection.entry.properties['cm:lockType']).toBe( + 'WRITE_LOCK' + ); + })); + + it('should unlock selection', fakeAsync(() => { + component.selection = { + entry: { + id: 'id', + name: 'test-name', + properties: { + 'cm:lockType': 'WRITE_LOCK' + } + } + }; + + spyOn(api.nodesApi, 'unlockNode').and.returnValue( + Promise.resolve({ + entry: { properties: {} } + }) + ); + + fixture.detectChanges(); + component.directive.onClick(); + + tick(); + fixture.detectChanges(); + + expect(component.selection.entry.properties['cm:lockType']).toBe(undefined); + })); +}); diff --git a/src/app/directives/edit-offline.directive.ts b/src/app/directives/edit-offline.directive.ts new file mode 100644 index 000000000..0902c5731 --- /dev/null +++ b/src/app/directives/edit-offline.directive.ts @@ -0,0 +1,105 @@ +/*! + * @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, + EventEmitter, + HostListener, + Input, + Output +} from '@angular/core'; +import { NodeEntry, NodeBodyLock, SharedLinkEntry } from '@alfresco/js-api'; +import { AlfrescoApiService } from '@alfresco/adf-core'; + +@Directive({ + selector: '[acaEditOffline]', + exportAs: 'editOffline' +}) +export class EditOfflineDirective { + @Input('acaEditOffline') + node: NodeEntry = null; + + @Output() toggle: EventEmitter = new EventEmitter(); + @Output() error: EventEmitter = new EventEmitter(); + + @HostListener('click') + onClick() { + this.toggleEdit(this.node); + } + + constructor(private alfrescoApiService: AlfrescoApiService) {} + + isNodeLocked() { + return !!( + this.node && + this.node.entry.properties && + this.node.entry.properties['cm:lockType'] === 'WRITE_LOCK' + ); + } + + private async toggleEdit(node: NodeEntry | SharedLinkEntry) { + const id = (node).entry.nodeId || node.entry.id; + if (this.isNodeLocked()) { + try { + const response = await this.unlockNode(id); + const isLocked = false; + + this.update(response.entry); + this.toggle.emit(isLocked); + } catch (error) { + this.error.emit(error); + } + } else { + try { + const response = await this.lockNode(id); + const isLocked = true; + + this.update(response.entry); + this.toggle.emit(isLocked); + } catch (error) { + this.error.emit(error); + } + } + } + + private lockNode(nodeId: string) { + return this.alfrescoApiService.nodesApi.lockNode(nodeId, { + type: 'ALLOW_OWNER_CHANGES', + lifetime: 'PERSISTENT' + }); + } + + private unlockNode(nodeId: string) { + return this.alfrescoApiService.nodesApi.unlockNode(nodeId); + } + + private update(data) { + const properties = this.node.entry.properties || {}; + + properties['cm:lockLifetime'] = data.properties['cm:lockLifetime']; + properties['cm:lockOwner'] = data.properties['cm:lockOwner']; + properties['cm:lockType'] = data.properties['cm:lockType']; + } +} diff --git a/src/app/extensions/core.extensions.module.ts b/src/app/extensions/core.extensions.module.ts index 64a6d65b5..6379e8db5 100644 --- a/src/app/extensions/core.extensions.module.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -45,12 +45,13 @@ import { LocationLinkComponent } from '../components/common/location-link/locati import { DocumentDisplayModeComponent } from '../components/toolbar/document-display-mode/document-display-mode.component'; import { ToggleJoinLibraryButtonComponent } from '../components/toolbar/toggle-join-library/toggle-join-library-button.component'; import { ToggleJoinLibraryMenuComponent } from '../components/toolbar/toggle-join-library/toggle-join-library-menu.component'; +import { ToggleEditOfflineComponent } from '../components/toolbar/toggle-edit-offline/toggle-edit-offline.component'; +import { CustomNameColumnComponent } from '../components/dl-custom-components/name-column/name-column.component'; import { LibraryNameColumnComponent, LibraryStatusColumnComponent, TrashcanNameColumnComponent, - LibraryRoleColumnComponent, - NameColumnComponent + LibraryRoleColumnComponent } from '@alfresco/adf-content-services'; export function setupExtensions(service: AppExtensionService): Function { @@ -95,12 +96,13 @@ export class CoreExtensionsModule { 'app.toolbar.cardView': DocumentDisplayModeComponent, 'app.menu.toggleJoinLibrary': ToggleJoinLibraryMenuComponent, 'app.shared-link.toggleSharedLink': ToggleSharedComponent, - 'app.columns.name': NameColumnComponent, + 'app.columns.name': CustomNameColumnComponent, 'app.columns.libraryName': LibraryNameColumnComponent, 'app.columns.libraryRole': LibraryRoleColumnComponent, 'app.columns.libraryStatus': LibraryStatusColumnComponent, 'app.columns.trashcanName': TrashcanNameColumnComponent, - 'app.columns.location': LocationLinkComponent + 'app.columns.location': LocationLinkComponent, + 'app.toolbar.toggleEditOffline': ToggleEditOfflineComponent }); extensions.setAuthGuards({ @@ -109,6 +111,7 @@ export class CoreExtensionsModule { extensions.setEvaluators({ 'app.selection.canDelete': app.canDeleteSelection, + 'app.selection.canEditLockedFile': app.canEditLockedFile, 'app.selection.canDownload': app.canDownloadSelection, 'app.selection.notEmpty': app.hasSelection, 'app.selection.canUnshare': app.canUnshareNodes, @@ -131,6 +134,8 @@ export class CoreExtensionsModule { 'app.navigation.isTrashcan': nav.isTrashcan, 'app.navigation.isNotTrashcan': nav.isNotTrashcan, 'app.navigation.isLibraries': nav.isLibraries, + 'app.navigation.isLibraryFiles': nav.isLibraryFiles, + 'app.navigation.isPersonalFiles': nav.isPersonalFiles, 'app.navigation.isNotLibraries': nav.isNotLibraries, 'app.navigation.isSharedFiles': nav.isSharedFiles, 'app.navigation.isNotSharedFiles': nav.isNotSharedFiles, diff --git a/src/app/extensions/evaluators/app.evaluators.ts b/src/app/extensions/evaluators/app.evaluators.ts index 8b7ebbb29..c767f9c6e 100644 --- a/src/app/extensions/evaluators/app.evaluators.ts +++ b/src/app/extensions/evaluators/app.evaluators.ts @@ -24,6 +24,7 @@ */ import { RuleContext, RuleParameter } from '@alfresco/adf-extensions'; +import { AppRuleContext } from '../app.interface'; import { isNotTrashcan, isNotLibraries, @@ -294,3 +295,27 @@ export function hasLockedFiles( ); }); } + +export function isWriteLocked( + context: AppRuleContext, + ...args: RuleParameter[] +): boolean { + return !!( + context.selection.file && + context.selection.file.entry && + context.selection.file.entry.properties && + context.selection.file.entry.properties['cm:lockType'] === 'WRITE_LOCK' + ); +} + +export function canEditLockedFile( + context: AppRuleContext, + ...args: RuleParameter[] +): boolean { + return !!( + !isWriteLocked(context, ...args) || + (context.selection.file.entry.properties['cm:lockOwner'] && + context.selection.file.entry.properties['cm:lockOwner'].id === + context.profile.id) + ); +} diff --git a/src/app/extensions/evaluators/navigation.evaluators.ts b/src/app/extensions/evaluators/navigation.evaluators.ts index 8955864f1..23ce69a2b 100644 --- a/src/app/extensions/evaluators/navigation.evaluators.ts +++ b/src/app/extensions/evaluators/navigation.evaluators.ts @@ -70,6 +70,22 @@ export function isNotTrashcan( return !isTrashcan(context, ...args); } +export function isPersonalFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/personal-files'); +} + +export function isLibraryFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/libraries'); +} + export function isLibraries( context: RuleContext, ...args: RuleParameter[] diff --git a/src/app/store/actions/node.actions.ts b/src/app/store/actions/node.actions.ts index 586736125..19be6d228 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -42,6 +42,7 @@ export const MANAGE_PERMISSIONS = 'MANAGE_PERMISSIONS'; export const MANAGE_VERSIONS = 'MANAGE_VERSIONS'; export const PRINT_FILE = 'PRINT_FILE'; export const FULLSCREEN_VIEWER = 'FULLSCREEN_VIEWER'; +export const EDIT_OFFLINE = 'EDIT_OFFLINE'; export class SetSelectedNodesAction implements Action { readonly type = SET_SELECTED_NODES; @@ -122,3 +123,8 @@ export class FullscreenViewerAction implements Action { readonly type = FULLSCREEN_VIEWER; constructor(public payload: MinimalNodeEntity) {} } + +export class EditOfflineAction implements Action { + readonly type = EDIT_OFFLINE; + constructor(public payload: any) {} +} diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 599721faa..3c8f81416 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -166,6 +166,7 @@ "parameters": [ { "type": "rule", "value": "app.selection.file" }, { "type": "rule", "value": "app.navigation.isNotTrashcan" }, + { "type": "rule", "value": "app.selection.canEditLockedFile" }, { "type": "rule", "value": "core.not", @@ -216,6 +217,34 @@ { "type": "rule", "value": "app.selection.canDownload" }, { "type": "rule", "value": "app.navigation.isNotTrashcan" } ] + }, + { + "id": "app.toolbar.canDelete", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.canDelete" }, + { "type": "rule", "value": "app.selection.canEditLockedFile" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" } + ] + }, + { + "id": "app.toolbar.canEditLockedFile", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.file" }, + { "type": "rule", "value": "app.selection.canEditLockedFile" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" }, + { + "type": "rule", + "value": "core.some", + "parameters": [ + { "type": "rule", "value": "app.navigation.isPreview" }, + { "type": "rule", "value": "app.navigation.isPersonalFiles" }, + { "type": "rule", "value": "app.navigation.isLibraryFiles" }, + { "type": "rule", "value": "app.navigation.isRecentFiles" } + ] + } + ] } ], @@ -503,11 +532,20 @@ "icon": "more_vert", "title": "APP.ACTIONS.MORE", "children": [ + { + "id": "app.toolbar.toggleEditOffline", + "order": 100, + "type": "custom", + "component": "app.toolbar.toggleEditOffline", + "rules": { + "visible": "app.toolbar.canEditLockedFile" + } + }, { "id": "app.toolbar.favorite", "comment": "workaround for Recent Files and Search API issue", "type": "custom", - "order": 100, + "order": 200, "component": "app.toolbar.toggleFavorite", "rules": { "visible": "app.toolbar.favorite.canToggle" @@ -516,7 +554,7 @@ { "id": "app.libraries.toolbar.toggleFavorite", "type": "custom", - "order": 101, + "order": 201, "component": "app.toolbar.toggleFavoriteLibrary", "rules": { "visible": "app.libraries.toolbar" @@ -524,7 +562,7 @@ }, { "id": "app.toolbar.favorite.add", - "order": 200, + "order": 300, "title": "APP.ACTIONS.FAVORITE", "icon": "star_border", "actions": { @@ -536,7 +574,7 @@ }, { "id": "app.toolbar.favorite.remove", - "order": 300, + "order": 400, "title": "APP.ACTIONS.FAVORITE", "icon": "star", "actions": { @@ -549,11 +587,11 @@ { "id": "app.create.separator.3", "type": "separator", - "order": 380 + "order": 3480 }, { "id": "app.toolbar.copy", - "order": 400, + "order": 500, "title": "APP.ACTIONS.COPY", "icon": "content_copy", "actions": { @@ -565,14 +603,14 @@ }, { "id": "app.toolbar.move", - "order": 500, + "order": 600, "title": "APP.ACTIONS.MOVE", "icon": "adf:move_file", "actions": { "click": "MOVE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { @@ -584,7 +622,7 @@ "click": "DELETE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { @@ -632,10 +670,19 @@ } ], "contextMenu": [ + { + "id": "app.context.toggleEditOffline", + "order": 100, + "type": "custom", + "component": "app.toolbar.toggleEditOffline", + "rules": { + "visible": "app.toolbar.canEditLockedFile" + } + }, { "id": "app.context.menu.share", "type": "custom", - "order": 100, + "order": 200, "component": "app.shared-link.toggleSharedLink", "rules": { "visible": "app.context.canShare" @@ -643,7 +690,7 @@ }, { "id": "app.context.menu.download", - "order": 200, + "order": 300, "title": "APP.ACTIONS.DOWNLOAD", "icon": "get_app", "actions": { @@ -655,7 +702,7 @@ }, { "id": "app.context.menu.preview", - "order": 300, + "order": 400, "title": "APP.ACTIONS.VIEW", "icon": "visibility", "actions": { @@ -667,7 +714,7 @@ }, { "id": "app.context.menu.editFolder", - "order": 400, + "order": 500, "title": "APP.ACTIONS.EDIT", "icon": "create", "actions": { @@ -680,7 +727,7 @@ { "id": "app.context.menu.favorite.add", "title": "APP.ACTIONS.FAVORITE", - "order": 500, + "order": 600, "icon": "star_border", "actions": { "click": "ADD_FAVORITE" @@ -692,7 +739,7 @@ { "id": "app.context.menu.favorite.remove", "title": "APP.ACTIONS.FAVORITE", - "order": 600, + "order": 700, "icon": "star", "actions": { "click": "REMOVE_FAVORITE" @@ -705,7 +752,7 @@ "id": "app.context.menu.favorite", "comment": "workaround for Recent Files and Search API issue", "type": "custom", - "order": 601, + "order": 701, "component": "app.toolbar.toggleFavorite", "rules": { "visible": "app.toolbar.favorite.canToggle" @@ -714,7 +761,7 @@ { "id": "app.context.menu.libraries.toggleFavorite", "type": "custom", - "order": 602, + "order": 702, "component": "app.toolbar.toggleFavoriteLibrary", "rules": { "visible": "app.libraries.toolbar" @@ -723,7 +770,7 @@ { "id": "app.context.menu.joinLibrary", "type": "custom", - "order": 603, + "order": 703, "component": "app.menu.toggleJoinLibrary", "rules": { "visible": "app.libraries.toolbar.canToggleJoin" @@ -731,7 +778,7 @@ }, { "id": "app.context.menu.leaveLibrary", - "order": 703, + "order": 803, "title": "APP.ACTIONS.LEAVE", "icon": "exit_to_app", "actions": { @@ -744,12 +791,12 @@ { "id": "app.create.separator.5", "type": "separator", - "order": 720 + "order": 820 }, { "id": "app.context.menu.copy", "title": "APP.ACTIONS.COPY", - "order": 750, + "order": 850, "icon": "content_copy", "actions": { "click": "COPY_NODES" @@ -761,30 +808,30 @@ { "id": "app.context.menu.move", "title": "APP.ACTIONS.MOVE", - "order": 800, + "order": 900, "icon": "adf:move_file", "actions": { "click": "MOVE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { "id": "app.context.menu.delete", "title": "APP.ACTIONS.DELETE", - "order": 900, + "order": 1000, "icon": "delete", "actions": { "click": "DELETE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { "id": "app.context.menu.deleteLibrary", - "order": 901, + "order": 1001, "title": "APP.ACTIONS.DELETE", "icon": "delete", "actions": { @@ -797,12 +844,12 @@ { "id": "app.create.separator.6", "type": "separator", - "order": 980 + "order": 1080 }, { "id": "app.context.menu.versions", "title": "APP.ACTIONS.VERSIONS", - "order": 1000, + "order": 1100, "icon": "history", "actions": { "click": "MANAGE_VERSIONS" @@ -815,7 +862,7 @@ "id": "app.context.menu.permissions", "title": "APP.ACTIONS.PERMISSIONS", "icon": "settings_input_component", - "order": 1100, + "order": 1200, "actions": { "click": "MANAGE_PERMISSIONS" }, @@ -825,7 +872,7 @@ }, { "id": "app.context.menu.purgeDeletedNodes", - "order": 1200, + "order": 1300, "title": "APP.ACTIONS.DELETE_PERMANENT", "icon": "delete_forever", "actions": { @@ -837,7 +884,7 @@ }, { "id": "app.context.menu.restoreDeletedNodes", - "order": 1300, + "order": 1400, "title": "APP.ACTIONS.RESTORE", "icon": "restore", "actions": { @@ -923,8 +970,17 @@ "title": "APP.ACTIONS.MORE", "children": [ { - "id": "app.viewer.favorite.add", + "id": "app.viewer.toggleEditOffline", "order": 100, + "type": "custom", + "component": "app.toolbar.toggleEditOffline", + "rules": { + "visible": "app.toolbar.canEditLockedFile" + } + }, + { + "id": "app.viewer.favorite.add", + "order": 200, "title": "APP.ACTIONS.FAVORITE", "icon": "star_border", "actions": { @@ -936,7 +992,7 @@ }, { "id": "app.viewer.favorite.remove", - "order": 200, + "order": 300, "title": "APP.ACTIONS.FAVORITE", "icon": "star", "actions": { @@ -950,7 +1006,7 @@ "id": "app.viewer.favorite", "comment": "workaround for Recent Files and Search API issue", "type": "custom", - "order": 101, + "order": 201, "component": "app.toolbar.toggleFavorite", "rules": { "visible": "app.toolbar.favorite.canToggle" @@ -959,7 +1015,7 @@ { "id": "app.viewer.more.separator.1", "type": "separator", - "order": 280 + "order": 380 }, { "id": "app.viewer.copy", @@ -982,7 +1038,7 @@ "click": "MOVE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { @@ -994,7 +1050,7 @@ "click": "DELETE_NODES" }, "rules": { - "visible": "app.selection.canDelete" + "visible": "app.toolbar.canDelete" } }, { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 04b6cad36..2eb5aaa9e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -202,7 +202,9 @@ "FULLSCREEN": "Activate full-screen mode", "JOIN": "Join", "CANCEL_JOIN": "Cancel join request", - "LEAVE": "Leave library" + "LEAVE": "Leave library", + "EDIT_OFFLINE": "Edit offline", + "EDIT_OFFLINE_CANCEL": "Cancel editing" }, "DIALOGS": { "CONFIRM_PURGE": { @@ -240,7 +242,7 @@ }, "MESSAGES": { "ERRORS":{ - "CANNOT_NAVIGATE_LOCATION": "Cannot open this location", + "CANNOT_NAVIGATE_LOCATION": "Cannot open this location", "MISSING_CONTENT": "This item no longer exists or you don't have permission to view it.", "GENERIC": "The action was unsuccessful. Try again or contact your IT Team.", "CONFLICT": "This name is already in use, try a different name.", @@ -251,6 +253,7 @@ "NODE_RESTORE": "{{ name }} couldn't be restored", "NODE_RESTORE_PLURAL": "{{ number }} items couldn't be restored", "PERMISSION": "You don't have access to do this", + "LOCK_NODE": "There was a problem locking the {{ fileName }} file", "TRASH": { "NODES_PURGE": { "PLURAL": "{{ number }} items couldn't be deleted", @@ -402,7 +405,6 @@ "LIBRARY_UPDATED": "Library properties updated" } }, - "SEARCH": { "INPUT": { "PLACEHOLDER": "Search",