Create a new activiti content component that provide the file preview (#1516)

* #1236 Add activiti content component

* fix wrong name

* Fix unit test import

* Rollback api changes

* #1236 fix preview using blob

* Fix after review
This commit is contained in:
Maurizio Vitale
2017-02-06 19:40:49 +00:00
committed by Mario Romano
parent 112d7a2d0e
commit d3d67c5934
17 changed files with 403 additions and 42 deletions

View File

@@ -351,6 +351,32 @@ There are two additional functions that can be of a great value when controlling
**Please note that if `event.preventDefault()` is not called then default outcome behaviour
will also be executed after your custom code.**
## Activiti Content Component
### Basic usage
The component shows the content preview.
```html
<activiti-content [contentId]="'1001'"></activiti-content>
```
### Configuration
#### Properties
The recommended set of properties can be found in the following table:
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `contentId` | string | | The content id to show. |
#### Events
| Name | Description |
| --- | --- |
| `contentClick` | Invoked when the content is clicked. |
## FormService
```ts

View File

@@ -19,6 +19,7 @@ import { NgModule, ModuleWithProviders } from '@angular/core';
import { CoreModule } from 'ng2-alfresco-core';
import { ActivitiForm } from './src/components/activiti-form.component';
import { ActivitiContent } from './src/components/activiti-content.component';
import { FormFieldComponent } from './src/components/form-field/form-field.component';
import { ActivitiStartForm } from './src/components/activiti-start-form.component';
import { FormService } from './src/services/form.service';
@@ -31,6 +32,7 @@ import { HttpModule } from '@angular/http';
import { WIDGET_DIRECTIVES } from './src/components/widgets/index';
export * from './src/components/activiti-form.component';
export * from './src/components/activiti-content.component';
export * from './src/components/activiti-start-form.component';
export * from './src/services/form.service';
export * from './src/components/widgets/index';
@@ -41,6 +43,7 @@ export * from './src/events/index';
export const ACTIVITI_FORM_DIRECTIVES: any[] = [
ActivitiForm,
ActivitiContent,
ActivitiStartForm,
FormFieldComponent,
...WIDGET_DIRECTIVES

View File

@@ -0,0 +1,42 @@
.upload-widget {
width: 100%;
word-break: break-all;
}
.upload-widget__icon {
float: left;
color: rgba(0, 0, 0, .26);
}
.upload-widget__file {
float: left;
margin-top: 4px;
color: rgba(0, 0, 0, .26);
}
.upload-widget__label {
color: rgba(0, 0, 0, .26);
}
.img-upload-widget {
width: 100%;
height: 100%;
border: 1px solid rgba(117, 117, 117, 0.57);
box-shadow: 1px 1px 2px #dddddd;
background-color: #ffffff;
}
.nothing-to-see {
box-shadow: 1px 1px 2px #dddddd;
background-color: #ffffff;
width: 100%;
height: 100%;
padding: 50px 0 50px 0;
text-align: center;
}
.previewTxt {
word-wrap: break-word;
word-break: break-all;
text-align: center;
}

View File

@@ -0,0 +1,22 @@
<div class="upload-widget" *ngIf="content">
<div class="mdl-card mdl-shadow--2dp">
<div class="mdl-card__title mdl-card--expand">
<div *ngIf="content.isThumbnailSupported()">
<img class="img-upload-widget" [src]="sanitizeUrl(content.thumbnailUrl)">
</div>
<div *ngIf="!content.isThumbnailSupported()">
<i class="material-icons">image</i>
<div class="previewTxt">{{ 'FORM.PREVIEW.IMAGE_NOT_AVAILABLE' | translate }}</div>
</div>
</div>
<div class="mdl-card__supporting-text">{{content.name}}</div>
<div class="mdl-card__actions mdl-card--border">
<button (click)="openViewer(content)" class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">zoom_in</i>
</button>
<a (click)="download($event)" [href]="content.contentRawUrl" target="_blank" [download]='content.name' class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">file_download</i>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,117 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
OnChanges,
SimpleChanges,
Input,
Output,
EventEmitter
} from '@angular/core';
import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { FormService } from './../services/form.service';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
moduleId: module.id,
selector: 'activiti-content',
templateUrl: './activiti-content.component.html',
styleUrls: ['./activiti-content.component.css']
})
export class ActivitiContent implements OnChanges {
@Input()
id: string;
@Output()
contentClick = new EventEmitter();
content: ContentLinkModel;
constructor(private translate: AlfrescoTranslationService,
protected formService: FormService,
private logService: LogService,
private sanitizer: DomSanitizer ) {
if (this.translate) {
this.translate.addTranslationFolder('ng2-activiti-form', 'node_modules/ng2-activiti-form/src');
}
}
ngOnChanges(changes: SimpleChanges) {
let contentId = changes['id'];
if (contentId && contentId.currentValue) {
this.loadContent(contentId.currentValue);
return;
}
}
loadContent(id: number) {
this.formService
.getFileContent(id)
.subscribe(
(response: ContentLinkModel) => {
this.content = new ContentLinkModel(response);
this.loadThumbnailUrl(this.content);
},
error => {
this.logService.error(error);
}
);
}
loadThumbnailUrl(content: ContentLinkModel) {
if (this.content.isTypeImage()) {
this.formService.getFileRawContent(content.id).subscribe(
(response: Blob) => {
this.content.thumbnailUrl = this.createUrlPreview(response);
},
error => {
this.logService.error(error);
}
);
} else if (this.content.isThumbnailSupported()) {
this.content.contentRawUrl = this.formService.getFileRawContentUrl(content.id);
this.content.thumbnailUrl = this.formService.getContentThumbnailUrl(content.id);
}
}
openViewer(content: ContentLinkModel) {
this.contentClick.emit(content);
this.logService.info('Content clicked' + content.id);
}
/**
* Download file opening it in a new window
*/
download($event) {
$event.stopPropagation();
}
private sanitizeUrl(url: string) {
return this.sanitizer.bypassSecurityTrustUrl(url);
}
private createUrlPreview(blob: Blob) {
let imageUrl = window.URL.createObjectURL(blob);
let sanitize: any = this.sanitizeUrl(imageUrl);
return sanitize.changingThisBreaksApplicationSecurity;
}
}

View File

@@ -21,6 +21,7 @@ import { Observable } from 'rxjs/Rx';
import { ActivitiStartForm } from './activiti-start-form.component';
import { FormFieldComponent } from './form-field/form-field.component';
import { ActivitiContent } from './activiti-content.component';
import { WIDGET_DIRECTIVES } from './widgets/index';
import { FormService } from './../services/form.service';
import { EcmModelService } from './../services/ecm-model.service';
@@ -45,6 +46,7 @@ describe('ActivitiStartForm', () => {
declarations: [
ActivitiStartForm,
FormFieldComponent,
ActivitiContent,
...WIDGET_DIRECTIVES
],
providers: [

View File

@@ -24,6 +24,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { WIDGET_DIRECTIVES } from '../index';
import { FormFieldComponent } from './../../form-field/form-field.component';
import { ActivitiContent } from './../../activiti-content.component';
import { fakeFormJson } from '../../../services/assets/widget-visibility.service.mock';
describe('ContainerWidget', () => {
@@ -132,7 +133,7 @@ describe('ContainerWidget', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [FormFieldComponent, WIDGET_DIRECTIVES]
declarations: [FormFieldComponent, ActivitiContent, WIDGET_DIRECTIVES]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(ContainerWidget);
containerWidgetComponent = fixture.componentInstance;

View File

@@ -0,0 +1,71 @@
/*!
* @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.
*/
export class ContentLinkModel {
contentAvailable: boolean;
created: string;
createdBy: any;
id: number;
link: boolean;
mimeType: string;
name: string;
previewStatus: string;
relatedContent: boolean;
simpleType: string;
thumbnailUrl: string;
contentRawUrl: string;
thumbnailStatus: string;
constructor(obj?: any) {
this.contentAvailable = obj && obj.contentAvailable;
this.created = obj && obj.created;
this.createdBy = obj && obj.createdBy || {};
this.id = obj && obj.id;
this.link = obj && obj.link;
this.mimeType = obj && obj.mimeType;
this.name = obj && obj.name;
this.previewStatus = obj && obj.previewStatus;
this.relatedContent = obj && obj.relatedContent;
this.simpleType = obj && obj.simpleType;
this.thumbnailStatus = obj && obj.thumbnailStatus;
}
hasPreviewStatus(): boolean {
return this.previewStatus === 'supported' ? true : false;
}
isTypeImage(): boolean {
return this.simpleType === 'image' ? true : false;
}
isTypePdf(): boolean {
return this.simpleType === 'pdf' ? true : false;
}
isTypeDoc(): boolean {
return this.simpleType === 'word' || this.simpleType === 'content' ? true : false;
}
isThumbnailReady(): boolean {
return this.thumbnailStatus === 'created';
}
isThumbnailSupported(): boolean {
return this.isTypeImage() || ((this.isTypePdf() || this.isTypeDoc()) && this.isThumbnailReady());
}
}

View File

@@ -29,3 +29,4 @@ export * from './tab.model';
export * from './form-outcome.model';
export * from './form-outcome-event.model';
export * from './form-field-validator';
export * from './content-link.model';

View File

@@ -21,32 +21,3 @@
width: 100%;
}
.upload-widget {
width: 100%;
word-break: break-all;
}
.upload-widget__icon {
float: left;
color: rgba(0, 0, 0, .26);
}
.upload-widget__file {
float: left;
margin-top: 4px;
color: rgba(0, 0, 0, .26);
}
.upload-widget__label {
color: rgba(0, 0, 0, .26);
}
.img-upload-widget {
width: 100px;
height: 100px;
padding: 2px;
border: 1px solid rgba(117, 117, 117, 0.57);
box-shadow: 1px 1px 2px #dddddd;
background-color: #ffffff;
}

View File

@@ -68,16 +68,9 @@
</div>
</div>
<div *ngSwitchCase="'upload'">
<div class="upload-widget">
<div>
<label class="upload-widget__label" [attr.for]="field.id">{{field.name}}</label>
</div>
<div>
<img *ngIf="hasFile" class="img-upload-widget" src="{{settingsService.bpmHost}}/activiti-app/app/rest/content/{{id}}/raw">
</div>
<div>
<i *ngIf="hasFile" class="material-icons upload-widget__icon">attachment</i>
<span *ngIf="hasFile" class="upload-widget__file">{{value}}</span>
<div *ngIf="hasFile" class="mdl-grid">
<div *ngFor="let file of field.value" class="mdl-cell mdl-cell--6-col">
<activiti-content [id]="file.id"></activiti-content>
</div>
</div>
</div>

View File

@@ -20,6 +20,7 @@ import { CoreModule, LogServiceMock } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
import { DisplayValueWidget } from './display-value.widget';
import { FormService } from '../../../services/form.service';
import { ActivitiContent } from '../../activiti-content.component';
import { EcmModelService } from '../../../services/ecm-model.service';
import { FormFieldModel } from './../core/form-field.model';
import { FormFieldTypes } from '../core/form-field-types';
@@ -688,7 +689,7 @@ describe('DisplayValueWidget', () => {
window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [DisplayValueWidget],
declarations: [DisplayValueWidget, ActivitiContent],
providers: [
EcmModelService,
FormService,

View File

@@ -23,6 +23,7 @@ import { TabsWidget } from './tabs.widget';
import { TabModel } from '../core/tab.model';
import { WIDGET_DIRECTIVES } from '../index';
import { FormFieldComponent } from './../../form-field/form-field.component';
import { ActivitiContent } from './../../activiti-content.component';
import { CoreModule } from 'ng2-alfresco-core';
describe('TabsWidget', () => {
@@ -103,7 +104,7 @@ describe('TabsWidget', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [FormFieldComponent, WIDGET_DIRECTIVES]
declarations: [FormFieldComponent, ActivitiContent, WIDGET_DIRECTIVES]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(TabsWidget);
tabWidgetComponent = fixture.componentInstance;

View File

@@ -2,6 +2,9 @@
"FORM": {
"START_FORM": {
"TITLE": "Start Form"
},
"PREVIEW": {
"IMAGE_NOT_AVAILABLE": "The preview image is not available."
}
}
}

View File

@@ -0,0 +1,10 @@
{
"FORM": {
"START_FORM": {
"TITLE": "Inizia"
},
"PREVIEW": {
"IMAGE_NOT_AVAILABLE": "Anteprima immagine non disponibile."
}
}
}

View File

@@ -30,6 +30,7 @@ describe('FormService', () => {
let service: FormService;
let apiService: AlfrescoApiService;
let logService: LogService;
let bpmCli: any;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -44,6 +45,7 @@ describe('FormService', () => {
});
service = TestBed.get(FormService);
apiService = TestBed.get(AlfrescoApiService);
bpmCli = apiService.getInstance().bpmAuth;
logService = TestBed.get(LogService);
});
@@ -374,6 +376,81 @@ describe('FormService', () => {
});
});
it('should return the unsupported content when the file is an image', (done) => {
let contentId: number = 999;
responseBody = {
id: contentId,
name: 'fake-name.jpg',
created: '2017-01-23T12:12:53.219+0000',
createdBy: {id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin'},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
};
service.getFileContent(contentId).subscribe(result => {
expect(result.id).toEqual(contentId);
expect(result.name).toEqual('fake-name.jpg');
expect(result.simpleType).toEqual('image');
expect(result.thumbnailStatus).toEqual('unsupported');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should return the supported content when the file is a pdf', (done) => {
let contentId: number = 888;
responseBody = {
id: contentId,
name: 'fake-name.pdf',
created: '2017-01-23T12:12:53.219+0000',
createdBy: {id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin'},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
};
service.getFileContent(contentId).subscribe(result => {
expect(result.id).toEqual(contentId);
expect(result.name).toEqual('fake-name.pdf');
expect(result.simpleType).toEqual('pdf');
expect(result.thumbnailStatus).toEqual('created');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should return the raw content URL', () => {
let contentId: number = 999;
let contentRawUrl = service.getFileRawContentUrl(contentId);
expect(contentRawUrl).toEqual(`${bpmCli.basePath}/api/enterprise/content/${contentId}/raw`);
});
it('should return the thumbnail URL', () => {
let contentId: number = 999;
let contentRawUrl = service.getContentThumbnailUrl(contentId);
expect(contentRawUrl).toEqual(`${bpmCli.basePath}/app/rest/content/${contentId}/rendition/thumbnail`);
});
it('should create a Form form a Node', (done) => {
let nameForm = 'testNode';

View File

@@ -252,6 +252,26 @@ export class FormService {
return Observable.fromPromise(this.apiService.getInstance().activiti.contentApi.createTemporaryRawRelatedContent(file));
}
getFileContent(contentId: number): Observable<any> {
let alfrescoApi = this.apiService.getInstance();
return Observable.fromPromise(alfrescoApi.activiti.contentApi.getContent(contentId));
}
getFileRawContent(contentId: number): Observable<any> {
let alfrescoApi = this.apiService.getInstance();
return Observable.fromPromise(alfrescoApi.activiti.contentApi.getRawContent(contentId));
}
getFileRawContentUrl(contentId: number): string {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.contentApi.getRawContentUrl(contentId);
}
getContentThumbnailUrl(contentId: number): string {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.contentApi.getContentThumbnailUrl(contentId);
}
getRestFieldValues(taskId: string, field: string): Observable<any> {
let alfrescoApi = this.apiService.getInstance();
return Observable.fromPromise(alfrescoApi.activiti.taskApi.getRestFieldValues(taskId, field));