Form: support for embedded document view (#1748)

* Form: support for embedded document view

* unit tests and improvements

- new ContentService to deal with trusted URL and downloads
- unit tests and improvements for ActivitiContent component
This commit is contained in:
Denys Vuika
2017-03-23 17:51:20 +00:00
committed by Mario Romano
parent a06138136b
commit 4f154f8bca
9 changed files with 221 additions and 65 deletions

View File

@@ -3,6 +3,10 @@
word-break: break-all; word-break: break-all;
} }
.upload-widget__content {
min-height: auto;
}
.upload-widget__icon { .upload-widget__icon {
float: left; float: left;
color: rgba(0, 0, 0, .26); color: rgba(0, 0, 0, .26);

View File

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

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.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { CoreModule } from 'ng2-alfresco-core';
import { ActivitiContent } from './activiti-content.component';
import { FormService } from './../services/form.service';
import { EcmModelService } from './../services/ecm-model.service';
import { ContentLinkModel } from './widgets/index';
describe('ActivitiContent', () => {
let fixture: ComponentFixture<ActivitiContent>;
let component: ActivitiContent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot()
],
declarations: [
ActivitiContent
],
providers: [
FormService,
EcmModelService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ActivitiContent);
component = fixture.componentInstance;
});
it('should display content thumbnail', () => {
component.showDocumentContent = true;
component.content = new ContentLinkModel();
fixture.detectChanges();
let content = fixture.debugElement.query(By.css('div.upload-widget__content-thumbnail'));
expect(content).toBeDefined();
});
it('should not display content thumbnail', () => {
component.showDocumentContent = false;
component.content = new ContentLinkModel();
fixture.detectChanges();
let content = fixture.debugElement.query(By.css('div.upload-widget__content-thumbnail'));
expect(content).toBeNull();
});
});

View File

@@ -15,18 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { Component, OnChanges, SimpleChanges, Input, Output, EventEmitter } from '@angular/core';
Component, import { AlfrescoTranslationService, LogService, ContentService } from 'ng2-alfresco-core';
OnChanges,
SimpleChanges,
Input,
Output,
EventEmitter
} from '@angular/core';
import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { FormService } from './../services/form.service'; import { FormService } from './../services/form.service';
import { ContentLinkModel } from './widgets/core/content-link.model'; import { ContentLinkModel } from './widgets/core/content-link.model';
import { DomSanitizer } from '@angular/platform-browser'; import { Observable } from 'rxjs/Rx';
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@@ -39,6 +32,9 @@ export class ActivitiContent implements OnChanges {
@Input() @Input()
id: string; id: string;
@Input()
showDocumentContent: boolean = false;
@Output() @Output()
contentClick = new EventEmitter(); contentClick = new EventEmitter();
@@ -47,19 +43,16 @@ export class ActivitiContent implements OnChanges {
constructor(private translate: AlfrescoTranslationService, constructor(private translate: AlfrescoTranslationService,
protected formService: FormService, protected formService: FormService,
private logService: LogService, private logService: LogService,
private sanitizer: DomSanitizer ) { private contentService: ContentService) {
if (this.translate) { if (this.translate) {
this.translate.addTranslationFolder('ng2-activiti-form', 'node_modules/ng2-activiti-form/src'); this.translate.addTranslationFolder('ng2-activiti-form', 'node_modules/ng2-activiti-form/src');
} }
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
let contentId = changes['id']; const contentId = changes['id'];
if (contentId && contentId.currentValue) { if (contentId && contentId.currentValue) {
this.loadContent(contentId.currentValue); this.loadContent(contentId.currentValue);
return;
} }
} }
@@ -79,19 +72,18 @@ export class ActivitiContent implements OnChanges {
loadThumbnailUrl(content: ContentLinkModel) { loadThumbnailUrl(content: ContentLinkModel) {
if (this.content.isThumbnailSupported()) { if (this.content.isThumbnailSupported()) {
let observable: Observable<any>;
if (this.content.isTypeImage()) { if (this.content.isTypeImage()) {
this.formService.getFileRawContent(content.id).subscribe( observable = this.formService.getFileRawContent(content.id);
(response: Blob) => {
this.content.thumbnailUrl = this.createUrlPreview(response);
},
error => {
this.logService.error(error);
}
);
} else { } else {
this.formService.getContentThumbnailUrl(content.id).subscribe( observable = this.formService.getContentThumbnailUrl(content.id);
}
if (observable) {
observable.subscribe(
(response: Blob) => { (response: Blob) => {
this.content.thumbnailUrl = this.createUrlPreview(response); this.content.thumbnailUrl = this.contentService.createTrustedUrl(response);
}, },
error => { error => {
this.logService.error(error); this.logService.error(error);
@@ -101,44 +93,18 @@ export class ActivitiContent implements OnChanges {
} }
} }
openViewer(content: ContentLinkModel) { openViewer(content: ContentLinkModel): void {
this.contentClick.emit(content); this.contentClick.emit(content);
this.logService.info('Content clicked' + content.id); this.logService.info('Content clicked' + content.id);
} }
/** /**
* Download file opening it in a new window * Invoke content download.
*/ */
download(content) { download(content: ContentLinkModel): void {
this.formService.getFileRawContent(content.id).subscribe( this.formService.getFileRawContent(content.id).subscribe(
(response: Blob) => { (blob: Blob) => this.contentService.downloadBlob(blob, content.name),
let thumbnailUrl = this.createUrlPreview(response); error => this.logService.error(error)
this.createDownloadElement(thumbnailUrl, content.name);
},
error => {
this.logService.error(error);
}
); );
} }
createDownloadElement(url: string, name: string) {
let downloadElement = window.document.createElement('a');
downloadElement.setAttribute('id', 'export-download');
downloadElement.setAttribute('href', url);
downloadElement.setAttribute('download', name);
downloadElement.setAttribute('target', '_blank');
window.document.body.appendChild(downloadElement);
downloadElement.click();
window.document.body.removeChild(downloadElement);
}
private sanitizeUrl(url: string) {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
private createUrlPreview(blob: Blob) {
let imageUrl = window.URL.createObjectURL(blob);
let sanitize: any = this.sanitizeUrl(imageUrl);
return sanitize.changingThisBreaksApplicationSecurity;
}
} }

View File

@@ -70,7 +70,7 @@
<div *ngSwitchCase="'upload'"> <div *ngSwitchCase="'upload'">
<div *ngIf="hasFile" class="mdl-grid"> <div *ngIf="hasFile" class="mdl-grid">
<div *ngFor="let file of field.value" class="mdl-cell mdl-cell--6-col"> <div *ngFor="let file of field.value" class="mdl-cell mdl-cell--6-col">
<activiti-content [id]="file.id"></activiti-content> <activiti-content [id]="file.id" [showDocumentContent]="showDocumentContent"></activiti-content>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -48,6 +48,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
// upload/attach // upload/attach
hasFile: boolean = false; hasFile: boolean = false;
showDocumentContent: boolean = false;
constructor(private formService: FormService, constructor(private formService: FormService,
private visibilityService: WidgetVisibilityService, private visibilityService: WidgetVisibilityService,
@@ -60,6 +61,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
this.value = this.field.value; this.value = this.field.value;
this.visibilityService.refreshEntityVisibility(this.field); this.visibilityService.refreshEntityVisibility(this.field);
if (this.field.params) { if (this.field.params) {
this.showDocumentContent = !!this.field.params['showDocumentContent'];
let originalField = this.field.params['field']; let originalField = this.field.params['field'];
if (originalField && originalField.type) { if (originalField && originalField.type) {
this.fieldType = originalField.type; this.fieldType = originalField.type;

View File

@@ -36,7 +36,8 @@ import {
AuthGuardBpm, AuthGuardBpm,
LogService, LogService,
LogServiceMock, LogServiceMock,
NotificationService NotificationService,
ContentService
} from './src/services/index'; } from './src/services/index';
import { UploadDirective } from './src/directives/upload.directive'; import { UploadDirective } from './src/directives/upload.directive';
@@ -66,6 +67,7 @@ export const ALFRESCO_CORE_PROVIDERS: any[] = [
AlfrescoTranslateLoader, AlfrescoTranslateLoader,
AlfrescoTranslationService, AlfrescoTranslationService,
RenditionsService, RenditionsService,
ContentService,
AuthGuard, AuthGuard,
AuthGuardEcm, AuthGuardEcm,
AuthGuardBpm, AuthGuardBpm,

View File

@@ -0,0 +1,109 @@
/*!
* @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 { Injectable } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Injectable()
export class ContentService {
private saveData: Function;
constructor(private sanitizer: DomSanitizer ) {
this.saveData = (function () {
let a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
return function (data, format, fileName) {
let blob = null;
if (format === 'blob') {
blob = data;
}
if (format === 'data') {
blob = new Blob([data], {type: 'octet/stream'});
}
if (format === 'object' || format === 'json') {
let json = JSON.stringify(data);
blob = new Blob([json], {type: 'octet/stream'});
}
if (blob) {
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
}
};
}());
}
/**
* Invokes content download for a Blob with a file name.
*
* @param {Blob} blob Content to download.
* @param {string} fileName Name of the resulting file.
*
* @memberOf ContentService
*/
downloadBlob(blob: Blob, fileName: string): void {
this.saveData(blob, 'blob', fileName);
}
/**
* Invokes content download for a data array with a file name.
*
* @param {*} data Data to download.
* @param {string} fileName Name of the resulting file.
*
* @memberOf ContentService
*/
downloadData(data: any, fileName: string): void {
this.saveData(data, 'data', fileName);
}
/**
* Invokes content download for a JSON object with a file name.
*
* @param {*} json JSON object to download.
* @param {any} fileName Name of the resulting file.
*
* @memberOf ContentService
*/
downloadJSON(json: any, fileName): void {
this.saveData(json, 'json', fileName);
}
/**
* Creates a trusted object URL from the Blob.
* WARNING: calling this method with untrusted user data exposes your application to XSS security risks!
* @param {Blob} blob Data to wrap into object URL
* @returns {string} Object URL content.
*
* @memberOf ContentService
*/
createTrustedUrl(blob: Blob): string {
let url = window.URL.createObjectURL(blob);
return <string> this.sanitizer.bypassSecurityTrustUrl(url);
}
}

View File

@@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
export * from './content.service';
export * from './storage.service'; export * from './storage.service';
export * from './alfresco-api.service'; export * from './alfresco-api.service';
export * from './alfresco-settings.service'; export * from './alfresco-settings.service';