diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 2c37d927d7..084ba7dd2e 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -244,10 +244,12 @@ + key="id"> - lock - lock_open + + lock + lock_open + + + ` + Opens a dialog to lock or unlock file + - `nodeEntry` - Item to lock or unlock. - `openFileBrowseDialogByFolderId(folderNodeId: string): Observable` Opens a file browser at a chosen folder location. - `folderNodeId` - ID of the folder to use diff --git a/docs/content-services/node-lock.directive.md b/docs/content-services/node-lock.directive.md new file mode 100644 index 0000000000..896473986f --- /dev/null +++ b/docs/content-services/node-lock.directive.md @@ -0,0 +1,22 @@ +--- +Added: v2.2.0 +Status: Active +--- +# Node Lock directive + +Call [`ContentNodeDialogService.openLockNodeDialog(nodeEntry)`](./content-node-dialog.service.md) method on click event, +and disable target button if provided node is not a file or user don't have permissions. + +## Basic Usage + +```html + + lock Lock file + +``` + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| node | `MinimalNodeEntryEntity` | | Node to lock. | diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts index c9cc5dcef1..9729b9288e 100644 --- a/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts +++ b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts @@ -22,6 +22,7 @@ import { DocumentListService } from '../document-list/services/document-list.ser import { ContentNodeDialogService } from './content-node-dialog.service'; import { MatDialog } from '@angular/material'; import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; const fakeNode: MinimalNodeEntryEntity = { id: 'fake', @@ -56,6 +57,7 @@ describe('ContentNodeDialogService', () => { let sitesService: SitesService; let materialDialog: MatDialog; let spyOnDialogOpen: jasmine.Spy; + let afterOpenObservable: Subject; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -66,6 +68,7 @@ describe('ContentNodeDialogService', () => { MatDialog ] }).compileComponents(); + })); beforeEach(() => { @@ -75,12 +78,29 @@ describe('ContentNodeDialogService', () => { service = TestBed.get(ContentNodeDialogService); documentListService = TestBed.get(DocumentListService); materialDialog = TestBed.get(MatDialog); - sitesService = TestBed.get(SitesService); - spyOnDialogOpen = spyOn(materialDialog, 'open').and.stub(); - spyOn(materialDialog, 'closeAll').and.stub(); + sitesService = TestBed.get(SitesService); + afterOpenObservable = new Subject(); + spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({ + afterOpen: () => afterOpenObservable, + afterClosed: () => Observable.of({}), + componentInstance: { + error: new Subject() + } + }); }); + it('should not open the lock node dialog if have no permission', () => { + const testNode: MinimalNodeEntryEntity = { + id: 'fake', + isFile: false + }; + + service.openLockNodeDialog(testNode).subscribe(() => {}, (error) => { + expect(error).toBe('OPERATION.FAIL.NODE.NO_PERMISSION'); + }); + }); + it('should be able to create the service', () => { expect(service).not.toBeNull(); }); @@ -123,6 +143,7 @@ describe('ContentNodeDialogService', () => { })); it('should be able to close the material dialog', () => { + spyOn(materialDialog, 'closeAll'); service.close(); expect(materialDialog.closeAll).toHaveBeenCalled(); }); diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.ts b/lib/content-services/content-node-selector/content-node-dialog.service.ts index ffc56398d7..096692f7c2 100644 --- a/lib/content-services/content-node-selector/content-node-dialog.service.ts +++ b/lib/content-services/content-node-selector/content-node-dialog.service.ts @@ -16,20 +16,24 @@ */ import { MatDialog } from '@angular/material'; -import { Injectable } from '@angular/core'; +import { EventEmitter, Injectable, Output } from '@angular/core'; import { ContentService } from '@alfresco/adf-core'; import { Subject } from 'rxjs/Subject'; import { Observable } from 'rxjs/Observable'; import { ShareDataRow } from '../document-list/data/share-data-row.model'; import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; -import { DataColumn, SitesService, TranslationService } from '@alfresco/adf-core'; +import { DataColumn, SitesService, TranslationService, PermissionsEnum } from '@alfresco/adf-core'; import { DocumentListService } from '../document-list/services/document-list.service'; import { ContentNodeSelectorComponent } from './content-node-selector.component'; import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface'; +import { NodeLockDialogComponent } from '../dialogs/node-lock.dialog'; @Injectable() export class ContentNodeDialogService { + @Output() + error: EventEmitter = new EventEmitter(); + constructor(private dialog: MatDialog, private contentService: ContentService, private documentListService: DocumentListService, @@ -45,6 +49,32 @@ export class ContentNodeDialogService { }); } + /** + * Opens a lock node dialog + * + * @param contentEntry Node to lock + */ + public openLockNodeDialog(contentEntry: MinimalNodeEntryEntity): Subject { + const observable: Subject = new Subject(); + + if (this.contentService.hasPermission(contentEntry, PermissionsEnum.LOCK)) { + this.dialog.open(NodeLockDialogComponent, { + data: { + node: contentEntry, + onError: (error) => { + this.error.emit(error); + observable.error(error); + } + }, + width: '400px' + }); + } else { + observable.error('OPERATION.FAIL.NODE.NO_PERMISSION'); + } + + return observable; + } + /** Opens a file browser at a chosen site location. */ openFileBrowseDialogBySite(): Observable { return this.siteService.getSites().switchMap((response: SitePaging) => { diff --git a/lib/content-services/dialogs/dialog.module.ts b/lib/content-services/dialogs/dialog.module.ts index 4c9c6a8328..5543566e17 100644 --- a/lib/content-services/dialogs/dialog.module.ts +++ b/lib/content-services/dialogs/dialog.module.ts @@ -21,32 +21,43 @@ import { MaterialModule } from '../material.module'; import { DownloadZipDialogComponent } from './download-zip.dialog'; import { FolderDialogComponent } from './folder.dialog'; +import { NodeLockDialogComponent } from './node-lock.dialog'; import { ShareDialogComponent } from './share.dialog'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; +import { FormModule } from '@alfresco/adf-core'; +import { MatDatetimepickerModule } from '@mat-datetimepicker/core'; +import { MatMomentDatetimeModule } from '@mat-datetimepicker/moment'; + @NgModule({ imports: [ CommonModule, MaterialModule, TranslateModule, FormsModule, - ReactiveFormsModule + FormModule, + ReactiveFormsModule, + MatMomentDatetimeModule, + MatDatetimepickerModule ], declarations: [ DownloadZipDialogComponent, FolderDialogComponent, + NodeLockDialogComponent, ShareDialogComponent ], exports: [ DownloadZipDialogComponent, FolderDialogComponent, + NodeLockDialogComponent, ShareDialogComponent ], entryComponents: [ DownloadZipDialogComponent, FolderDialogComponent, + NodeLockDialogComponent, ShareDialogComponent ] }) diff --git a/lib/content-services/dialogs/node-lock.dialog.html b/lib/content-services/dialogs/node-lock.dialog.html new file mode 100644 index 0000000000..27eb8f3a50 --- /dev/null +++ b/lib/content-services/dialogs/node-lock.dialog.html @@ -0,0 +1,47 @@ + + {{ 'CORE.FILE_DIALOG.FILE_LOCK' | translate }} + + + + + + + {{ 'CORE.FILE_DIALOG.FILE_LOCK_CHECKBOX' | translate }} "{{ nodeName }}" + + + + + + + {{ 'CORE.FILE_DIALOG.ALLOW_OTHERS_CHECKBOX' | translate }} + + + + + + {{ 'CORE.FILE_DIALOG.TIME_LOCK_CHECKBOX' | translate }} + + + + + + + + + + + + + + + + + + + {{ 'CORE.FILE_DIALOG.CANCEL_BUTTON.LABEL' | translate }} + + + + {{ 'CORE.FILE_DIALOG.SAVE_BUTTON.LABEL' | translate }} + + diff --git a/lib/content-services/dialogs/node-lock.dialog.spec.ts b/lib/content-services/dialogs/node-lock.dialog.spec.ts new file mode 100644 index 0000000000..45efa8d024 --- /dev/null +++ b/lib/content-services/dialogs/node-lock.dialog.spec.ts @@ -0,0 +1,161 @@ +/*! + * @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 moment from 'moment-es6'; + +import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef } from '@angular/material'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { Observable } from 'rxjs/Observable'; + +import { AlfrescoApiService, TranslationService } from '@alfresco/adf-core'; +import { NodeLockDialogComponent } from './node-lock.dialog'; + +describe('NodeLockDialogComponent', () => { + + let fixture: ComponentFixture; + let component: NodeLockDialogComponent; + let translationService: TranslationService; + let alfrescoApi: AlfrescoApiService; + let expiryDate; + const dialogRef = { + close: jasmine.createSpy('close') + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + BrowserDynamicTestingModule + ], + declarations: [ + NodeLockDialogComponent + ], + providers: [ + { provide: MatDialogRef, useValue: dialogRef } + ] + }); + + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { entryComponents: [NodeLockDialogComponent] } + }); + + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NodeLockDialogComponent); + component = fixture.componentInstance; + + alfrescoApi = TestBed.get(AlfrescoApiService); + + translationService = TestBed.get(TranslationService); + spyOn(translationService, 'get').and.returnValue(Observable.of('message')); + }); + + describe('Node lock dialog component', () => { + + beforeEach(() => { + jasmine.clock().mockDate(new Date()); + expiryDate = moment().add(1, 'minutes'); + + component.data = { + node: { + id: 'node-id', + name: 'node-name', + isLocked: true, + properties: { + ['cm:lockType']: 'WRITE_LOCK', + ['cm:expiryDate']: expiryDate + } + }, + onError: () => {} + }; + fixture.detectChanges(); + }); + + it('should init dialog with form inputs', () => { + expect(component.nodeName).toBe('node-name'); + expect(component.form.value.isLocked).toBe(true); + expect(component.form.value.allowOwner).toBe(true); + expect(component.form.value.isTimeLock).toBe(true); + expect(component.form.value.time.format()).toBe(expiryDate.format()); + }); + + it('should update form inputs', () => { + let newTime = moment(); + component.form.controls['isLocked'].setValue(false); + component.form.controls['allowOwner'].setValue(false); + component.form.controls['isTimeLock'].setValue(false); + component.form.controls['time'].setValue(newTime); + + expect(component.form.value.isLocked).toBe(false); + expect(component.form.value.allowOwner).toBe(false); + expect(component.form.value.isTimeLock).toBe(false); + expect(component.form.value.time.format()).toBe(newTime.format()); + }); + + it('should submit the form and lock the node', () => { + spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.resolve({})); + + component.submit(); + + expect(alfrescoApi.nodesApi.lockNode).toHaveBeenCalledWith( + 'node-id', + { + 'timeToExpire': 60, + 'type': 'ALLOW_OWNER_CHANGES', + 'lifetime': 'PERSISTENT' + } + ); + }); + + it('should submit the form and unlock the node', () => { + spyOn(alfrescoApi.nodesApi, 'unlockNode').and.returnValue(Promise.resolve({})); + + component.form.controls['isLocked'].setValue(false); + component.submit(); + + expect(alfrescoApi.nodesApi.unlockNode).toHaveBeenCalledWith('node-id'); + }); + + it('should call dialog to close with form data when submit is succesfluly', fakeAsync(() => { + const node = { entry: {} }; + spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.resolve(node)); + + component.submit(); + tick(); + fixture.detectChanges(); + + expect(dialogRef.close).toHaveBeenCalledWith(node.entry); + })); + + it('should call onError if submit fails', fakeAsync(() => { + spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.reject('error')); + spyOn(component.data, 'onError'); + + component.submit(); + tick(); + fixture.detectChanges(); + + expect(component.data.onError).toHaveBeenCalled(); + })); + }); +}); diff --git a/lib/content-services/dialogs/node-lock.dialog.ts b/lib/content-services/dialogs/node-lock.dialog.ts new file mode 100644 index 0000000000..f34fe51889 --- /dev/null +++ b/lib/content-services/dialogs/node-lock.dialog.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 moment from 'moment-es6'; + +import { Component, Inject, OnInit, Optional } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +import { MinimalNodeEntryEntity, NodeEntry } from 'alfresco-js-api'; +import { AlfrescoApiService } from '@alfresco/adf-core'; + +@Component({ + selector: 'adf-node-lock', + styleUrls: ['./folder.dialog.scss'], + templateUrl: './node-lock.dialog.html' +}) +export class NodeLockDialogComponent implements OnInit { + + form: FormGroup; + node: MinimalNodeEntryEntity = null; + nodeName: string; + + constructor( + private formBuilder: FormBuilder, + public dialog: MatDialogRef, + private alfrescoApi: AlfrescoApiService, + @Optional() + @Inject(MAT_DIALOG_DATA) + public data: any + ) {} + + ngOnInit() { + const { node } = this.data; + this.nodeName = node.name; + + this.form = this.formBuilder.group({ + isLocked: node.isLocked || false, + allowOwner: node.properties['cm:lockType'] === 'WRITE_LOCK', + isTimeLock: !!node.properties['cm:expiryDate'], + time: !!node.properties['cm:expiryDate'] ? moment(node.properties['cm:expiryDate']) : moment() + }); + } + + private get lockTimeInSeconds(): number { + if (this.form.value.isTimeLock) { + let duration = moment.duration(moment(this.form.value.time).diff(moment())); + return duration.asSeconds(); + } + + return 0; + } + + private get nodeBodyLock(): object { + return { + 'timeToExpire': this.lockTimeInSeconds, + 'type': this.form.value.allowOwner ? 'ALLOW_OWNER_CHANGES' : 'FULL', + 'lifetime': 'PERSISTENT' + }; + } + + private toggleLock(): Promise { + const { alfrescoApi: { nodesApi }, data: { node } } = this; + + if (this.form.value.isLocked) { + return nodesApi.lockNode(node.id, this.nodeBodyLock); + } + + return nodesApi.unlockNode(node.id); + } + + submit(): void { + this.toggleLock() + .then(node => { + this.data.node.isLocked = this.form.value.isLocked; + this.dialog.close(node.entry); + }) + .catch(error => this.data.onError(error)); + } +} diff --git a/lib/content-services/dialogs/public-api.ts b/lib/content-services/dialogs/public-api.ts index 6152a9d30b..c61ea6a428 100644 --- a/lib/content-services/dialogs/public-api.ts +++ b/lib/content-services/dialogs/public-api.ts @@ -17,4 +17,5 @@ export * from './download-zip.dialog'; export * from './folder.dialog'; +export * from './node-lock.dialog'; export * from './share.dialog'; diff --git a/lib/content-services/directives/content-directive.module.ts b/lib/content-services/directives/content-directive.module.ts index 8041afb301..c21ec8b345 100644 --- a/lib/content-services/directives/content-directive.module.ts +++ b/lib/content-services/directives/content-directive.module.ts @@ -21,6 +21,7 @@ import { MaterialModule } from '../material.module'; import { NodeDownloadDirective } from './node-download.directive'; import { NodeSharedDirective } from './node-share.directive'; +import { NodeLockDirective } from './node-lock.directive'; @NgModule({ imports: [ @@ -29,11 +30,13 @@ import { NodeSharedDirective } from './node-share.directive'; ], declarations: [ NodeDownloadDirective, - NodeSharedDirective + NodeSharedDirective, + NodeLockDirective ], exports: [ NodeDownloadDirective, - NodeSharedDirective + NodeSharedDirective, + NodeLockDirective ] }) export class ContentDirectiveModule { diff --git a/lib/content-services/directives/node-lock.directive.spec.ts b/lib/content-services/directives/node-lock.directive.spec.ts new file mode 100644 index 0000000000..0cf0859dc8 --- /dev/null +++ b/lib/content-services/directives/node-lock.directive.spec.ts @@ -0,0 +1,99 @@ +/*! + * @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 { TestBed, ComponentFixture, async, fakeAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Component, DebugElement } from '@angular/core'; + +import { NodeLockDirective } from './node-lock.directive'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { NodeActionsService } from '../document-list/services/node-actions.service'; +import { ContentNodeDialogService } from '../content-node-selector/content-node-dialog.service'; +import { DocumentListService } from '../document-list/services/document-list.service'; + +const fakeNode: MinimalNodeEntryEntity = { + id: 'fake', + isFile: true, + isLocked: false +}; + +@Component({ + template: '' +}) +class TestComponent { + node = null; +} + +describe('NodeLock Directive', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let element: DebugElement; + let contentNodeDialogService: ContentNodeDialogService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + NodeActionsService, + ContentNodeDialogService, + DocumentListService + ], + declarations: [ + TestComponent, + NodeLockDirective + ] + }); + + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + element = fixture.debugElement.query(By.directive(NodeLockDirective)); + contentNodeDialogService = TestBed.get(ContentNodeDialogService); + }); + + it('should call openLockNodeDialog method on click', () => { + spyOn(contentNodeDialogService, 'openLockNodeDialog'); + component.node = fakeNode; + + fixture.detectChanges(); + element = fixture.debugElement.query(By.directive(NodeLockDirective)); + element.triggerEventHandler('click', { + preventDefault: () => {} + }); + + expect(contentNodeDialogService.openLockNodeDialog).toHaveBeenCalledWith(fakeNode); + }); + + it('should disable the button if node is a folder', fakeAsync(() => { + component.node = { isFile: false, isFolder: true }; + + fixture.detectChanges(); + + expect(element.nativeElement.disabled).toEqual(true); + })); + + it('should enable the button if node is a file', fakeAsync(() => { + component.node = { isFile: true, isFolder: false }; + + fixture.detectChanges(); + + expect(element.nativeElement.disabled).toEqual(false); + })); +}); diff --git a/lib/content-services/directives/node-lock.directive.ts b/lib/content-services/directives/node-lock.directive.ts new file mode 100644 index 0000000000..96b33c4c56 --- /dev/null +++ b/lib/content-services/directives/node-lock.directive.ts @@ -0,0 +1,50 @@ +/*! + * @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. + */ + +/* tslint:disable:no-input-rename */ + +import { Directive, ElementRef, Renderer2, HostListener, Input, AfterViewInit } from '@angular/core'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { PermissionsEnum, ContentService } from '@alfresco/adf-core'; +import { ContentNodeDialogService } from '../content-node-selector/content-node-dialog.service'; + +@Directive({ + selector: '[adf-node-lock]' +}) +export class NodeLockDirective implements AfterViewInit { + + @Input('adf-node-lock') + node: MinimalNodeEntryEntity; + + @HostListener('click', [ '$event' ]) + onClick(event) { + event.preventDefault(); + this.contentNodeDialogService.openLockNodeDialog(this.node); + } + + constructor( + public element: ElementRef, + private renderer: Renderer2, + private contentService: ContentService, + private contentNodeDialogService: ContentNodeDialogService + ) {} + + ngAfterViewInit() { + const hasPermission = this.contentService.hasPermission(this.node, PermissionsEnum.LOCK); + this.renderer.setProperty(this.element.nativeElement, 'disabled', !hasPermission); + } +} diff --git a/lib/content-services/directives/node-share.directive.spec.ts b/lib/content-services/directives/node-share.directive.spec.ts index 4c681a67ef..116f921397 100644 --- a/lib/content-services/directives/node-share.directive.spec.ts +++ b/lib/content-services/directives/node-share.directive.spec.ts @@ -107,7 +107,7 @@ describe('NodeSharedDirective', () => { })); it('should enable the button if nodes is selected and is a file', fakeAsync(() => { - component.node = { entry: { id: '1', name: 'name1' isFolder: false, isFile: true } }; + component.node = { entry: { id: '1', name: 'name1', isFolder: false, isFile: true } }; fixture.detectChanges(); diff --git a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts index 3cbaf330d0..22c163c740 100644 --- a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts +++ b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts @@ -60,8 +60,8 @@ describe('ContentAction', () => { beforeEach(() => { contentService = TestBed.get(ContentService); - nodeActionsService = new NodeActionsService(null, null); - documentActions = new DocumentActionsService(nodeActionsService); + nodeActionsService = new NodeActionsService(null, null, null); + documentActions = new DocumentActionsService(nodeActionsService, null); folderActions = new FolderActionsService(nodeActionsService, null, contentService); documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent); diff --git a/lib/content-services/document-list/components/document-list.component.ts b/lib/content-services/document-list/components/document-list.component.ts index 05ef9e5e3d..82a863f55d 100644 --- a/lib/content-services/document-list/components/document-list.component.ts +++ b/lib/content-services/document-list/components/document-list.component.ts @@ -25,7 +25,8 @@ import { ObjectDataColumn, PaginatedComponent, PaginationQueryParams, - PermissionsEnum + PermissionsEnum, + ContentService } from '@alfresco/adf-core'; import { AlfrescoApiService, @@ -255,7 +256,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte private elementRef: ElementRef, private apiService: AlfrescoApiService, private appConfig: AppConfigService, - private preferences: UserPreferencesService) { + private preferences: UserPreferencesService, + private contentService?: ContentService) { this.maxItems = this.preferences.paginationSize; this.pagination = new BehaviorSubject( { @@ -433,7 +435,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte } checkPermission(node: any, action: ContentActionModel): ContentActionModel { - if (action.permission && action.permission !== PermissionsEnum.COPY) { + if (action.permission && !~[PermissionsEnum.COPY, PermissionsEnum.LOCK].indexOf(action.permission)) { if (this.hasPermissions(node)) { let permissions = node.entry.allowableOperations; let findPermission = permissions.find(permission => permission === action.permission); @@ -442,6 +444,11 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte } } } + + if (action.permission === PermissionsEnum.LOCK) { + action.disabled = !this.contentService.hasPermission(node.entry, PermissionsEnum.LOCK); + } + return action; } diff --git a/lib/content-services/document-list/services/document-actions.service.spec.ts b/lib/content-services/document-list/services/document-actions.service.spec.ts index c5cc797849..87d0a6400c 100644 --- a/lib/content-services/document-list/services/document-actions.service.spec.ts +++ b/lib/content-services/document-list/services/document-actions.service.spec.ts @@ -39,13 +39,17 @@ describe('DocumentActionsService', () => { let alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()); documentListService = new DocumentListService(null, contentService, alfrescoApiService, null, null); - service = new DocumentActionsService(null, documentListService, contentService); + service = new DocumentActionsService(null, null, documentListService, contentService); }); it('should register default download action', () => { expect(service.getHandler('download')).not.toBeNull(); }); + it('should register lock action', () => { + expect(service.getHandler('lock')).toBeDefined(); + }); + it('should register custom action handler', () => { let handler: ContentActionHandler = function (obj: any) {}; service.setHandler('', handler); @@ -71,7 +75,7 @@ describe('DocumentActionsService', () => { let file = new FileNode(); expect(service.canExecuteAction(file)).toBeTruthy(); - service = new DocumentActionsService(nodeActionsService); + service = new DocumentActionsService(nodeActionsService, null); expect(service.canExecuteAction(file)).toBeFalsy(); }); diff --git a/lib/content-services/document-list/services/document-actions.service.ts b/lib/content-services/document-list/services/document-actions.service.ts index bbd88d0c95..fe83684dc9 100644 --- a/lib/content-services/document-list/services/document-actions.service.ts +++ b/lib/content-services/document-list/services/document-actions.service.ts @@ -24,6 +24,7 @@ import { ContentActionHandler } from '../models/content-action.model'; import { PermissionModel } from '../models/permissions.model'; import { DocumentListService } from './document-list.service'; import { NodeActionsService } from './node-actions.service'; +import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service'; import 'rxjs/add/observable/throw'; @Injectable() @@ -36,6 +37,7 @@ export class DocumentActionsService { private handlers: { [id: string]: ContentActionHandler; } = {}; constructor(private nodeActionsService: NodeActionsService, + private contentNodeDialogService: ContentNodeDialogService, private documentListService?: DocumentListService, private contentService?: ContentService) { this.setupActionHandlers(); @@ -83,6 +85,11 @@ export class DocumentActionsService { this.handlers['move'] = this.moveNode.bind(this); this.handlers['delete'] = this.deleteNode.bind(this); this.handlers['download'] = this.downloadNode.bind(this); + this.handlers['lock'] = this.lockNode.bind(this); + } + + private lockNode(node: MinimalNodeEntity, target?: any, permission?: string) { + return this.contentNodeDialogService.openLockNodeDialog(node.entry); } private downloadNode(obj: MinimalNodeEntity, target?: any, permission?: string) { diff --git a/lib/content-services/document-list/services/node-actions.service.service.spec.ts b/lib/content-services/document-list/services/node-actions.service.service.spec.ts index 2b8f68ff09..d25b4278ad 100644 --- a/lib/content-services/document-list/services/node-actions.service.service.spec.ts +++ b/lib/content-services/document-list/services/node-actions.service.service.spec.ts @@ -22,9 +22,12 @@ import { DocumentListService } from './document-list.service'; import { NodeActionsService } from './node-actions.service'; import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service'; import { Observable } from 'rxjs/Observable'; +import { MatDialogRef } from '@angular/material'; +import { NodeLockDialogComponent } from '../../dialogs/node-lock.dialog'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; const fakeNode: MinimalNodeEntryEntity = { - id: 'fake' + id: 'fake' }; describe('NodeActionsService', () => { @@ -32,15 +35,28 @@ describe('NodeActionsService', () => { let service: NodeActionsService; let documentListService: DocumentListService; let contentDialogService: ContentNodeDialogService; + const dialogRef = { + open: jasmine.createSpy('open') + }; beforeEach(async(() => { TestBed.configureTestingModule({ + declarations: [ + NodeLockDialogComponent + ], + imports: [], providers: [ NodeActionsService, DocumentListService, - ContentNodeDialogService + ContentNodeDialogService, + { provide: MatDialogRef, useValue: dialogRef } ] + }); + + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { entryComponents: [ NodeLockDialogComponent ] } }).compileComponents(); + })); beforeEach(() => { diff --git a/lib/content-services/document-list/services/node-actions.service.ts b/lib/content-services/document-list/services/node-actions.service.ts index 6c1762581c..d869eac9ee 100644 --- a/lib/content-services/document-list/services/node-actions.service.ts +++ b/lib/content-services/document-list/services/node-actions.service.ts @@ -15,10 +15,10 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, Output, EventEmitter } from '@angular/core'; import { MinimalNodeEntryEntity, MinimalNodeEntity } from 'alfresco-js-api'; import { Subject } from 'rxjs/Subject'; -import { AlfrescoApiService } from '@alfresco/adf-core'; +import { AlfrescoApiService, ContentService } from '@alfresco/adf-core'; import { MatDialog } from '@angular/material'; import { DocumentListService } from './document-list.service'; @@ -28,7 +28,12 @@ import { NodeDownloadDirective } from '../../directives/node-download.directive' @Injectable() export class NodeActionsService { + @Output() + error: EventEmitter = new EventEmitter(); + constructor(private contentDialogService: ContentNodeDialogService, + public dialogRef: MatDialog, + public content: ContentService, private documentListService?: DocumentListService, private apiService?: AlfrescoApiService, private dialog?: MatDialog) {} diff --git a/lib/content-services/material.module.ts b/lib/content-services/material.module.ts index eccbe975f3..c2e576f45c 100644 --- a/lib/content-services/material.module.ts +++ b/lib/content-services/material.module.ts @@ -31,8 +31,9 @@ import { MatRippleModule, MatExpansionModule, MatSelectModule, - MatSlideToggleModule, - MatCheckboxModule + MatCheckboxModule, + MatDatepickerModule, + MatSlideToggleModule } from '@angular/material'; export function modules() { @@ -51,8 +52,9 @@ export function modules() { MatOptionModule, MatExpansionModule, MatSelectModule, - MatSlideToggleModule, - MatCheckboxModule + MatCheckboxModule, + MatDatepickerModule, + MatSlideToggleModule ]; } diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index 9f2577c48d..4faade3b67 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -51,6 +51,18 @@ "TITLE": "Adding files to zip, this could take a few minutes" } }, + "FILE_DIALOG": { + "FILE_LOCK": "Lock file", + "ALLOW_OTHERS_CHECKBOX": "Allow the owner to modify this file", + "FILE_LOCK_CHECKBOX": "Lock file", + "TIME_LOCK_CHECKBOX": "Time lock", + "SAVE_BUTTON": { + "LABEL": "Save" + }, + "CANCEL_BUTTON": { + "LABEL": "Cancel" + } + }, "FOLDER_DIALOG": { "CREATE_FOLDER_TITLE": "Create new folder", "EDIT_FOLDER_TITLE": "Edit folder", diff --git a/lib/core/models/permissions.enum.ts b/lib/core/models/permissions.enum.ts index 42035ed551..68a7095940 100644 --- a/lib/core/models/permissions.enum.ts +++ b/lib/core/models/permissions.enum.ts @@ -20,6 +20,7 @@ export class PermissionsEnum extends String { static UPDATE: string = 'update'; static CREATE: string = 'create'; static COPY: string = 'copy'; + static LOCK: string = 'lock'; static UPDATEPERMISSIONS: string = 'updatePermissions'; static NOT_DELETE: string = '!delete'; static NOT_UPDATE: string = '!update'; diff --git a/lib/core/services/content.service.ts b/lib/core/services/content.service.ts index 8a3de5099c..5793e414ac 100644 --- a/lib/core/services/content.service.ts +++ b/lib/core/services/content.service.ts @@ -202,9 +202,9 @@ export class ContentService { if (this.hasAllowableOperations(node)) { if (permission && permission.startsWith('!')) { - hasPermission = node.allowableOperations.find(currentPermission => currentPermission === permission.replace('!', '')) ? false : true; + hasPermission = !~node.allowableOperations.indexOf(permission.replace('!', '')); } else { - hasPermission = node.allowableOperations.find(currentPermission => currentPermission === permission) ? true : false; + hasPermission = !!~node.allowableOperations.indexOf(permission); } } else { @@ -217,6 +217,14 @@ export class ContentService { hasPermission = true; } + if (permission === PermissionsEnum.LOCK) { + hasPermission = node.isFile; + + if (node.isLocked && this.hasAllowableOperations(node)) { + hasPermission = !!~node.allowableOperations.indexOf('updatePermissions'); + } + } + return hasPermission; }