[ADF-2540] Lock node feature (#3138)

* add adf-node-lock directive

* add lock-node service + button in context menu

* unit tests

* docs

* unit tests fix

* Remove unnecessary imports

* PR changes

* Remove fit from tests

* Update specific node from list on lock/ulock
This commit is contained in:
Alex Bolboșenco
2018-04-06 08:59:28 +03:00
committed by Denys Vuika
parent 7b7e39d989
commit 7d1b4bf14a
26 changed files with 643 additions and 29 deletions

View File

@@ -244,10 +244,12 @@
<data-column
class="desktop-only"
title="{{'DOCUMENT_LIST.COLUMNS.IS_LOCKED' | translate}}"
key="isLocked">
key="id">
<ng-template let-entry="$implicit">
<mat-icon *ngIf="entry.data.getValue(entry.row, entry.col)">lock</mat-icon>
<mat-icon *ngIf="!entry.data.getValue(entry.row, entry.col)">lock_open</mat-icon>
<button mat-icon-button [adf-node-lock]="entry.row.node.entry">
<mat-icon *ngIf="entry.row.getValue('isLocked')">lock</mat-icon>
<mat-icon *ngIf="!entry.row.getValue('isLocked')">lock_open</mat-icon>
</button>
</ng-template>
</data-column>
<data-column
@@ -325,6 +327,12 @@
(error)="onContentActionError($event)"
(execute)="onPermissionRequested($event)">
</content-action>
<content-action
icon="lock"
permission="lock"
handler="lock"
title="Lock">
</content-action>
</content-actions>
</adf-document-list>
<adf-pagination

View File

@@ -217,6 +217,7 @@ for more information about installing and using the source code.
| [Folder edit directive](content-services/folder-edit.directive.md) | Allows folders to be edited. | [Source](../lib/content-services/folder-directive/folder-edit.directive.ts) |
| [Inherited button directive](content-services/inherited-button.directive.md) | Update the current node by adding/removing the inherited permissions. | [Source](../lib/content-services/permission-manager/components/inherited-button.directive.ts) |
| [File draggable directive](core/file-draggable.directive.md) | Provide drag-and-drop features for an element such as a `div`. | [Source](../lib/content-services/upload/directives/file-draggable.directive.ts) |
| [Node lock directive](content-services/node-lock.directive.md) | Open the node lock dialog on click. | [Source](../lib/content-services/directives/node-lock.directive.ts) |
## Models

View File

@@ -45,6 +45,7 @@ for more information about installing and using the source code.
| [Folder edit directive](folder-edit.directive.md) | Allows folders to be edited. | [Source](../../lib/content-services/folder-directive/folder-edit.directive.ts) |
| [Inherited button directive](inherited-button.directive.md) | Update the current node by adding/removing the inherited permissions. | [Source](../../lib/content-services/permission-manager/components/inherited-button.directive.ts) |
| [File draggable directive](file-draggable.directive.md) | Provide drag-and-drop features for an element such as a `div`. | [Source](../../lib/content-services/upload/directives/file-draggable.directive.ts) |
| [Node lock directive](node-lock.directive.md) | Open the node lock dialog on click. | [Source](../../lib/content-services/directives/node-lock.directive.ts) |
## Models

View File

@@ -10,6 +10,9 @@ Displays and manages dialogs for selecting content to open, copy or upload.
## Methods
- `openLockNodeDialog(nodeEntry: MinimalNodeEntryEntity): Observable<string>`
Opens a dialog to lock or unlock file
- `nodeEntry` - Item to lock or unlock.
- `openFileBrowseDialogByFolderId(folderNodeId: string): Observable<MinimalNodeEntryEntity[]>`
Opens a file browser at a chosen folder location.
- `folderNodeId` - ID of the folder to use

View File

@@ -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
<button mat-icon-button [adf-node-lock]="node.entry">
<mat-icon>lock</mat-icon> Lock file
</button>
```
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| node | `MinimalNodeEntryEntity` | | Node to lock. |

View File

@@ -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 = <MinimalNodeEntryEntity> {
id: 'fake',
@@ -56,6 +57,7 @@ describe('ContentNodeDialogService', () => {
let sitesService: SitesService;
let materialDialog: MatDialog;
let spyOnDialogOpen: jasmine.Spy;
let afterOpenObservable: Subject<any>;
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<any>();
spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({
afterOpen: () => afterOpenObservable,
afterClosed: () => Observable.of({}),
componentInstance: {
error: new Subject<any>()
}
});
});
it('should not open the lock node dialog if have no permission', () => {
const testNode: MinimalNodeEntryEntity = <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();
});

View File

@@ -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<any> = new EventEmitter<any>();
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<string> {
const observable: Subject<string> = new Subject<string>();
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<MinimalNodeEntryEntity[]> {
return this.siteService.getSites().switchMap((response: SitePaging) => {

View File

@@ -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
]
})

View File

@@ -0,0 +1,47 @@
<h2 mat-dialog-title>
{{ 'CORE.FILE_DIALOG.FILE_LOCK' | translate }}
</h2>
<mat-dialog-content>
<br />
<form [formGroup]="form" (submit)="submit()">
<mat-checkbox [formControl]="form.controls['isLocked']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.FILE_LOCK_CHECKBOX' | translate }} <strong>"{{ nodeName }}"</strong>
</mat-checkbox>
<br />
<div *ngIf="form.value.isLocked">
<mat-checkbox [formControl]="form.controls['allowOwner']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.ALLOW_OTHERS_CHECKBOX' | translate }}
</mat-checkbox>
<br />
<mat-checkbox [formControl]="form.controls['isTimeLock']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.TIME_LOCK_CHECKBOX' | translate }}
</mat-checkbox>
<br />
<mat-form-field *ngIf="form.value.isTimeLock">
<mat-datetimepicker-toggle [for]="datetimePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker type="datetime" openOnFocus="true" timeInterval="1"></mat-datetimepicker>
<input matInput [formControl]="form.controls['time']" [matDatetimepicker]="datetimePicker" required autocomplete="false">
</mat-form-field>
</div>
</form>
<br />
</mat-dialog-content>
<mat-dialog-actions class="adf-dialog-buttons">
<span class="adf-fill-remaining-space"></span>
<button mat-button mat-dialog-close>
{{ 'CORE.FILE_DIALOG.CANCEL_BUTTON.LABEL' | translate }}
</button>
<button class="adf-dialog-action-button" mat-button (click)="submit()">
{{ 'CORE.FILE_DIALOG.SAVE_BUTTON.LABEL' | translate }}
</button>
</mat-dialog-actions>

View File

@@ -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<NodeLockDialogComponent>;
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();
}));
});
});

View File

@@ -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<NodeLockDialogComponent>,
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<NodeEntry> {
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));
}
}

View File

@@ -17,4 +17,5 @@
export * from './download-zip.dialog';
export * from './folder.dialog';
export * from './node-lock.dialog';
export * from './share.dialog';

View File

@@ -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 {

View File

@@ -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 = <MinimalNodeEntryEntity> {
id: 'fake',
isFile: true,
isLocked: false
};
@Component({
template: '<div [adf-node-lock]="node"></div>'
})
class TestComponent {
node = null;
}
describe('NodeLock Directive', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
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);
}));
});

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<Pagination>(<Pagination> {
@@ -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;
}

View File

@@ -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('<key>', 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();
});

View File

@@ -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) {

View File

@@ -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 = <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(() => {

View File

@@ -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<any> = new EventEmitter<any>();
constructor(private contentDialogService: ContentNodeDialogService,
public dialogRef: MatDialog,
public content: ContentService,
private documentListService?: DocumentListService,
private apiService?: AlfrescoApiService,
private dialog?: MatDialog) {}

View File

@@ -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
];
}

View File

@@ -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",

View File

@@ -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';

View File

@@ -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;
}