mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
committed by
Denys Vuika
parent
7b7e39d989
commit
7d1b4bf14a
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
22
docs/content-services/node-lock.directive.md
Normal file
22
docs/content-services/node-lock.directive.md
Normal 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. |
|
@@ -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(() => {
|
||||
@@ -76,11 +79,28 @@ describe('ContentNodeDialogService', () => {
|
||||
documentListService = TestBed.get(DocumentListService);
|
||||
materialDialog = TestBed.get(MatDialog);
|
||||
sitesService = TestBed.get(SitesService);
|
||||
spyOnDialogOpen = spyOn(materialDialog, 'open').and.stub();
|
||||
spyOn(materialDialog, 'closeAll').and.stub();
|
||||
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();
|
||||
});
|
||||
|
@@ -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) => {
|
||||
|
@@ -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
|
||||
]
|
||||
})
|
||||
|
47
lib/content-services/dialogs/node-lock.dialog.html
Normal file
47
lib/content-services/dialogs/node-lock.dialog.html
Normal 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>
|
161
lib/content-services/dialogs/node-lock.dialog.spec.ts
Normal file
161
lib/content-services/dialogs/node-lock.dialog.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
||||
});
|
94
lib/content-services/dialogs/node-lock.dialog.ts
Normal file
94
lib/content-services/dialogs/node-lock.dialog.ts
Normal 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));
|
||||
}
|
||||
}
|
@@ -17,4 +17,5 @@
|
||||
|
||||
export * from './download-zip.dialog';
|
||||
export * from './folder.dialog';
|
||||
export * from './node-lock.dialog';
|
||||
export * from './share.dialog';
|
||||
|
@@ -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 {
|
||||
|
99
lib/content-services/directives/node-lock.directive.spec.ts
Normal file
99
lib/content-services/directives/node-lock.directive.spec.ts
Normal 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);
|
||||
}));
|
||||
});
|
50
lib/content-services/directives/node-lock.directive.ts
Normal file
50
lib/content-services/directives/node-lock.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -22,6 +22,9 @@ 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'
|
||||
@@ -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(() => {
|
||||
|
@@ -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) {}
|
||||
|
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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';
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user