From 4f154f8bca4f17523390db6f618c6d4925978c93 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Thu, 23 Mar 2017 17:51:20 +0000 Subject: [PATCH] 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 --- .../components/activiti-content.component.css | 4 + .../activiti-content.component.html | 15 +-- .../activiti-content.component.spec.ts | 71 ++++++++++++ .../components/activiti-content.component.ts | 78 ++++--------- .../display-value/display-value.widget.html | 2 +- .../display-value/display-value.widget.ts | 2 + ng2-components/ng2-alfresco-core/index.ts | 4 +- .../src/services/content.service.ts | 109 ++++++++++++++++++ .../ng2-alfresco-core/src/services/index.ts | 1 + 9 files changed, 221 insertions(+), 65 deletions(-) create mode 100644 ng2-components/ng2-activiti-form/src/components/activiti-content.component.spec.ts create mode 100644 ng2-components/ng2-alfresco-core/src/services/content.service.ts diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.css b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.css index 219afdd91a..76bc08da82 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.css +++ b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.css @@ -3,6 +3,10 @@ word-break: break-all; } +.upload-widget__content { + min-height: auto; +} + .upload-widget__icon { float: left; color: rgba(0, 0, 0, .26); diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.html b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.html index 72435d7454..668dedeffb 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.html +++ b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.html @@ -1,22 +1,23 @@
-
-
+
+
- +
image
{{ 'FORM.PREVIEW.IMAGE_NOT_AVAILABLE' | translate }}
-
{{content.name}}
-
+
{{content.name}}
+ +
-
+
+
diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.spec.ts b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.spec.ts new file mode 100644 index 0000000000..71d54a0f94 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.spec.ts @@ -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; + 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(); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.ts b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.ts index 577ba69763..531f9fdc9b 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-content.component.ts +++ b/ng2-components/ng2-activiti-form/src/components/activiti-content.component.ts @@ -15,18 +15,11 @@ * limitations under the License. */ -import { - Component, - OnChanges, - SimpleChanges, - Input, - Output, - EventEmitter -} from '@angular/core'; -import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core'; +import { Component, OnChanges, SimpleChanges, Input, Output, EventEmitter } from '@angular/core'; +import { AlfrescoTranslationService, LogService, ContentService } from 'ng2-alfresco-core'; import { FormService } from './../services/form.service'; import { ContentLinkModel } from './widgets/core/content-link.model'; -import { DomSanitizer } from '@angular/platform-browser'; +import { Observable } from 'rxjs/Rx'; @Component({ moduleId: module.id, @@ -39,6 +32,9 @@ export class ActivitiContent implements OnChanges { @Input() id: string; + @Input() + showDocumentContent: boolean = false; + @Output() contentClick = new EventEmitter(); @@ -47,19 +43,16 @@ export class ActivitiContent implements OnChanges { constructor(private translate: AlfrescoTranslationService, protected formService: FormService, private logService: LogService, - private sanitizer: DomSanitizer ) { - + private contentService: ContentService) { if (this.translate) { this.translate.addTranslationFolder('ng2-activiti-form', 'node_modules/ng2-activiti-form/src'); } - } ngOnChanges(changes: SimpleChanges) { - let contentId = changes['id']; + const contentId = changes['id']; if (contentId && contentId.currentValue) { this.loadContent(contentId.currentValue); - return; } } @@ -79,19 +72,18 @@ export class ActivitiContent implements OnChanges { loadThumbnailUrl(content: ContentLinkModel) { if (this.content.isThumbnailSupported()) { + let observable: Observable; + if (this.content.isTypeImage()) { - this.formService.getFileRawContent(content.id).subscribe( - (response: Blob) => { - this.content.thumbnailUrl = this.createUrlPreview(response); - }, - error => { - this.logService.error(error); - } - ); + observable = this.formService.getFileRawContent(content.id); } else { - this.formService.getContentThumbnailUrl(content.id).subscribe( + observable = this.formService.getContentThumbnailUrl(content.id); + } + + if (observable) { + observable.subscribe( (response: Blob) => { - this.content.thumbnailUrl = this.createUrlPreview(response); + this.content.thumbnailUrl = this.contentService.createTrustedUrl(response); }, 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.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( - (response: Blob) => { - let thumbnailUrl = this.createUrlPreview(response); - this.createDownloadElement(thumbnailUrl, content.name); - }, - error => { - this.logService.error(error); - } + (blob: Blob) => this.contentService.downloadBlob(blob, 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; - } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.html index 161beaab36..375c3a884b 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.html @@ -70,7 +70,7 @@
- +
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.ts index d9e3751c60..3828614e65 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/display-value/display-value.widget.ts @@ -48,6 +48,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit { // upload/attach hasFile: boolean = false; + showDocumentContent: boolean = false; constructor(private formService: FormService, private visibilityService: WidgetVisibilityService, @@ -60,6 +61,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit { this.value = this.field.value; this.visibilityService.refreshEntityVisibility(this.field); if (this.field.params) { + this.showDocumentContent = !!this.field.params['showDocumentContent']; let originalField = this.field.params['field']; if (originalField && originalField.type) { this.fieldType = originalField.type; diff --git a/ng2-components/ng2-alfresco-core/index.ts b/ng2-components/ng2-alfresco-core/index.ts index a6aa07a0ba..e4e36ec27c 100644 --- a/ng2-components/ng2-alfresco-core/index.ts +++ b/ng2-components/ng2-alfresco-core/index.ts @@ -36,7 +36,8 @@ import { AuthGuardBpm, LogService, LogServiceMock, - NotificationService + NotificationService, + ContentService } from './src/services/index'; import { UploadDirective } from './src/directives/upload.directive'; @@ -66,6 +67,7 @@ export const ALFRESCO_CORE_PROVIDERS: any[] = [ AlfrescoTranslateLoader, AlfrescoTranslationService, RenditionsService, + ContentService, AuthGuard, AuthGuardEcm, AuthGuardBpm, diff --git a/ng2-components/ng2-alfresco-core/src/services/content.service.ts b/ng2-components/ng2-alfresco-core/src/services/content.service.ts new file mode 100644 index 0000000000..cad5a87725 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/services/content.service.ts @@ -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 this.sanitizer.bypassSecurityTrustUrl(url); + } + +} diff --git a/ng2-components/ng2-alfresco-core/src/services/index.ts b/ng2-components/ng2-alfresco-core/src/services/index.ts index 56390bea87..905cbbd57b 100644 --- a/ng2-components/ng2-alfresco-core/src/services/index.ts +++ b/ng2-components/ng2-alfresco-core/src/services/index.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +export * from './content.service'; export * from './storage.service'; export * from './alfresco-api.service'; export * from './alfresco-settings.service';