diff --git a/lib/content-services/document-list/data/share-datatable-adapter.spec.ts b/lib/content-services/document-list/data/share-datatable-adapter.spec.ts index c4624e64d0..8149a9f63e 100644 --- a/lib/content-services/document-list/data/share-datatable-adapter.spec.ts +++ b/lib/content-services/document-list/data/share-datatable-adapter.spec.ts @@ -15,12 +15,14 @@ * limitations under the License. */ -import { DataColumn, DataRow, DataSorting, ContentService, ThumbnailService } from '@alfresco/adf-core'; +import { DataColumn, DataRow, DataSorting, ContentService, ThumbnailService, setupTestBed } from '@alfresco/adf-core'; import { FileNode, FolderNode, SmartFolderNode, RuleFolderNode, LinkFolderNode } from './../../mock'; import { ShareDataRow } from './share-data-row.model'; import { ShareDataTableAdapter } from './share-datatable-adapter'; import { DomSanitizer } from '@angular/platform-browser'; import { MatIconRegistry } from '@angular/material'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { TestBed } from '@angular/core/testing'; class FakeSanitizer extends DomSanitizer { @@ -57,12 +59,26 @@ describe('ShareDataTableAdapter', () => { let thumbnailService: ThumbnailService; let contentService: ContentService; - const fakeMatIconRegistry: MatIconRegistry = jasmine.createSpyObj(['addSvgIcon', 'addSvgIconInNamespace']); + + setupTestBed({ + imports: [ContentTestingModule], + providers: [ + { + provide: MatIconRegistry, + useValue: jasmine.createSpyObj(['addSvgIcon', 'addSvgIconInNamespace']) + }, + { + provide: DomSanitizer, useClass: FakeSanitizer + } + ] + }); beforeEach(() => { const imageUrl: string = 'http://'; - contentService = new ContentService(null, null, null, null); - thumbnailService = new ThumbnailService(contentService, fakeMatIconRegistry, new FakeSanitizer()); + + contentService = TestBed.get(ContentService); + thumbnailService = TestBed.get(ThumbnailService); + spyOn(thumbnailService, 'getDocumentThumbnailUrl').and.returnValue(imageUrl); }); diff --git a/lib/content-services/document-list/services/document-actions.service.spec.ts b/lib/content-services/document-list/services/document-actions.service.spec.ts index 837d65d6de..73ef5866e0 100644 --- a/lib/content-services/document-list/services/document-actions.service.spec.ts +++ b/lib/content-services/document-list/services/document-actions.service.spec.ts @@ -16,13 +16,14 @@ */ import { AlfrescoApiServiceMock, AppConfigService, ContentService, - setupTestBed, CoreModule, TranslationMock, StorageService + setupTestBed, CoreModule, TranslationMock, AlfrescoApiService, StorageService } from '@alfresco/adf-core'; import { FileNode, FolderNode } from '../../mock'; import { ContentActionHandler } from '../models/content-action.model'; import { DocumentActionsService } from './document-actions.service'; import { DocumentListService } from './document-list.service'; import { of } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; describe('DocumentActionsService', () => { @@ -30,14 +31,18 @@ describe('DocumentActionsService', () => { let documentListService: DocumentListService; setupTestBed({ - imports: [ - CoreModule.forRoot() + imports: [CoreModule.forRoot()], + providers: [ + { + provide: AlfrescoApiService, + useValue: new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()) + } ] }); beforeEach(() => { - const contentService = new ContentService(null, null, null, null); const alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()); + const contentService = TestBed.get(ContentService); documentListService = new DocumentListService(contentService, alfrescoApiService, null, null); service = new DocumentActionsService(null, null, new TranslationMock(), documentListService, contentService); diff --git a/lib/content-services/document-list/services/document-list.service.spec.ts b/lib/content-services/document-list/services/document-list.service.spec.ts index 483960c518..efc4c64f01 100644 --- a/lib/content-services/document-list/services/document-list.service.spec.ts +++ b/lib/content-services/document-list/services/document-list.service.spec.ts @@ -19,6 +19,7 @@ import { AlfrescoApiServiceMock, AlfrescoApiService, AppConfigService, ContentService, setupTestBed, CoreModule, LogService, AppConfigServiceMock, StorageService } from '@alfresco/adf-core'; import { DocumentListService } from './document-list.service'; import { CustomResourcesService } from './custom-resources.service'; +import { TestBed } from '@angular/core/testing'; declare let jasmine: any; @@ -69,7 +70,7 @@ describe('DocumentListService', () => { beforeEach(() => { const logService = new LogService(new AppConfigServiceMock(null)); - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()); const customActionService = new CustomResourcesService(alfrescoApiService, logService); service = new DocumentListService(contentService, alfrescoApiService, logService, customActionService); diff --git a/lib/content-services/document-list/services/folder-actions.service.spec.ts b/lib/content-services/document-list/services/folder-actions.service.spec.ts index d42e7d2f46..82d91aab55 100644 --- a/lib/content-services/document-list/services/folder-actions.service.spec.ts +++ b/lib/content-services/document-list/services/folder-actions.service.spec.ts @@ -39,7 +39,7 @@ describe('FolderActionsService', () => { const appConfig: AppConfigService = TestBed.get(AppConfigService); appConfig.config.ecmHost = 'http://localhost:9876/ecm'; - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); const alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()); documentListService = new DocumentListService(contentService, alfrescoApiService, null, null); service = new FolderActionsService(null, documentListService, contentService, new TranslationMock()); diff --git a/lib/core/directives/check-allowable-operation.directive.spec.ts b/lib/core/directives/check-allowable-operation.directive.spec.ts index 31af2a930a..aa80f435ae 100644 --- a/lib/core/directives/check-allowable-operation.directive.spec.ts +++ b/lib/core/directives/check-allowable-operation.directive.spec.ts @@ -18,6 +18,9 @@ import { ChangeDetectorRef, Component, ElementRef, SimpleChange } from '@angular/core'; import { ContentService } from './../services/content.service'; import { CheckAllowableOperationDirective, NodeAllowableOperationSubject } from './check-allowable-operation.directive'; +import { setupTestBed } from '../testing/setupTestBed'; +import { CoreModule } from '../core.module'; +import { TestBed } from '@angular/core/testing'; @Component({ selector: 'adf-text-subject' @@ -30,6 +33,10 @@ describe('CheckAllowableOperationDirective', () => { let changeDetectorMock: ChangeDetectorRef; + setupTestBed({ + imports: [CoreModule.forRoot()] + }); + beforeEach(() => { changeDetectorMock = { detectChanges: () => {} }; }); @@ -101,7 +108,7 @@ describe('CheckAllowableOperationDirective', () => { }); it('enables element when all nodes have expected permission', () => { - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); spyOn(contentService, 'hasAllowableOperations').and.returnValue(true); const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock); @@ -114,7 +121,7 @@ describe('CheckAllowableOperationDirective', () => { }); it('disables element when one of the nodes have no permission', () => { - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); spyOn(contentService, 'hasAllowableOperations').and.returnValue(false); const directive = new CheckAllowableOperationDirective(null, null, contentService, changeDetectorMock); @@ -130,7 +137,7 @@ describe('CheckAllowableOperationDirective', () => { describe('Angular component as subject', () => { it('disables decorated component', () => { - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); spyOn(contentService, 'hasAllowableOperations').and.returnValue(false); spyOn(changeDetectorMock, 'detectChanges'); @@ -146,7 +153,7 @@ describe('CheckAllowableOperationDirective', () => { }); it('enables decorated component', () => { - const contentService = new ContentService(null, null, null, null); + const contentService = TestBed.get(ContentService); spyOn(contentService, 'hasAllowableOperations').and.returnValue(true); spyOn(changeDetectorMock, 'detectChanges'); diff --git a/lib/core/services/content.service.spec.ts b/lib/core/services/content.service.spec.ts index 526443331c..8efdf830a5 100644 --- a/lib/core/services/content.service.spec.ts +++ b/lib/core/services/content.service.spec.ts @@ -79,7 +79,7 @@ describe('ContentService', () => { it('should return a valid content URL', (done) => { authService.login('fake-username', 'fake-password').subscribe(() => { - expect(contentService.getContentUrl(node)).toBe('http://localhost:9876/ecm/alfresco/api/' + + expect(contentService.getContentUrl(node)).toContain('/ecm/alfresco/api/' + '-default-/public/alfresco/versions/1/nodes/fake-node-id/content?attachment=false&alf_ticket=fake-post-ticket'); done(); }); @@ -94,7 +94,7 @@ describe('ContentService', () => { it('should return a valid thumbnail URL', (done) => { authService.login('fake-username', 'fake-password').subscribe(() => { expect(contentService.getDocumentThumbnailUrl(node)) - .toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco' + + .toContain('/ecm/alfresco/api/-default-/public/alfresco' + '/versions/1/nodes/fake-node-id/renditions/doclib/content?attachment=false&alf_ticket=fake-post-ticket'); done(); }); @@ -178,19 +178,4 @@ describe('ContentService', () => { expect(contentService.hasPermissions(permissionNode, '!Consumer')).toBeFalsy(); }); }); - - describe('Download blob', () => { - - it('Should use native msSaveOrOpenBlob if the browser is IE', (done) => { - - const navigatorAny: any = window.navigator; - - navigatorAny.__defineGetter__('msSaveOrOpenBlob', () => { - done(); - }); - - const blob = new Blob([''], { type: 'text/html' }); - contentService.downloadBlob(blob, 'test_ie'); - }); - }); }); diff --git a/lib/core/services/content.service.ts b/lib/core/services/content.service.ts index 38bec4f3c9..5e7613e718 100644 --- a/lib/core/services/content.service.ts +++ b/lib/core/services/content.service.ts @@ -26,14 +26,14 @@ import { LogService } from './log.service'; import { catchError } from 'rxjs/operators'; import { PermissionsEnum } from '../models/permissions.enum'; import { AllowableOperationsEnum } from '../models/allowable-operations.enum'; +import { DownloadService } from './download.service'; +import { ThumbnailService } from './thumbnail.service'; @Injectable({ providedIn: 'root' }) export class ContentService { - private saveData: Function; - folderCreated: Subject = new Subject(); folderCreate: Subject = new Subject(); folderEdit: Subject = new Subject(); @@ -41,66 +41,39 @@ export class ContentService { constructor(public authService: AuthenticationService, public apiService: AlfrescoApiService, private logService: LogService, - private sanitizer: DomSanitizer) { - this.saveData = (function () { - const a = document.createElement('a'); - document.body.appendChild(a); - a.style.display = 'none'; - - return function (fileData, format, fileName) { - let blob = null; - - if (format === 'blob' || format === 'data') { - blob = new Blob([fileData], { type: 'octet/stream' }); - } - - if (format === 'object' || format === 'json') { - const json = JSON.stringify(fileData); - blob = new Blob([json], { type: 'octet/stream' }); - } - - if (blob) { - - if (typeof window.navigator !== 'undefined' && window.navigator.msSaveOrOpenBlob) { - navigator.msSaveOrOpenBlob(blob, fileName); - } else { - const url = window.URL.createObjectURL(blob); - a.href = url; - a.download = fileName; - a.click(); - - window.URL.revokeObjectURL(url); - } - } - }; - }()); + private sanitizer: DomSanitizer, + private downloadService: DownloadService, + private thumbnailService: ThumbnailService) { } /** + * @deprecated in 3.2.0, use DownloadService instead. * Invokes content download for a Blob with a file name. * @param blob Content to download. * @param fileName Name of the resulting file. */ downloadBlob(blob: Blob, fileName: string): void { - this.saveData(blob, 'blob', fileName); + this.downloadService.downloadBlob(blob, fileName); } /** + * @deprecated in 3.2.0, use DownloadService instead. * Invokes content download for a data array with a file name. * @param data Data to download. * @param fileName Name of the resulting file. */ downloadData(data: any, fileName: string): void { - this.saveData(data, 'data', fileName); + this.downloadService.downloadData(data, fileName); } /** + * @deprecated in 3.2.0, use DownloadService instead. * Invokes content download for a JSON object with a file name. * @param json JSON object to download. * @param fileName Name of the resulting file. */ downloadJSON(json: any, fileName: string): void { - this.saveData(json, 'json', fileName); + this.downloadService.downloadJSON(json, fileName); } /** @@ -119,35 +92,38 @@ export class ContentService { } /** + * @deprecated in 3.2.0, use ThumbnailService instead. * Gets a thumbnail URL for the given document node. - * @param node Node to get URL for. + * @param node Node or Node ID to get URL for. * @param attachment Toggles whether to retrieve content as an attachment for download * @param ticket Custom ticket to use for authentication * @returns URL string */ - getDocumentThumbnailUrl(node: any, attachment?: boolean, ticket?: string): string { - - if (node && node.entry) { - node = node.entry.id; - } - - return this.contentApi.getDocumentThumbnailUrl(node, attachment, ticket); + getDocumentThumbnailUrl(node: NodeEntry | string, attachment?: boolean, ticket?: string): string { + return this.thumbnailService.getDocumentThumbnailUrl(node, attachment, ticket); } /** * Gets a content URL for the given node. - * @param node Node to get URL for. + * @param node Node or Node ID to get URL for. * @param attachment Toggles whether to retrieve content as an attachment for download * @param ticket Custom ticket to use for authentication - * @returns URL string + * @returns URL string or `null` */ - getContentUrl(node: any, attachment?: boolean, ticket?: string): string { + getContentUrl(node: NodeEntry | string, attachment?: boolean, ticket?: string): string { + if (node) { + let nodeId: string; - if (node && node.entry) { - node = node.entry.id; + if (typeof node === 'string') { + nodeId = node; + } else if (node.entry) { + nodeId = node.entry.id; + } + + return this.contentApi.getContentUrl(nodeId, attachment, ticket); } - return this.contentApi.getContentUrl(node, attachment, ticket); + return null; } /** diff --git a/lib/core/services/download.service.spec.ts b/lib/core/services/download.service.spec.ts new file mode 100644 index 0000000000..48252c2a73 --- /dev/null +++ b/lib/core/services/download.service.spec.ts @@ -0,0 +1,39 @@ +/*! + * @license + * Copyright 2019 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 { DownloadService } from './download.service'; + +describe('DownloadService', () => { + let service: DownloadService; + + beforeEach(() => { + service = new DownloadService(); + }); + + describe('Download blob', () => { + it('Should use native msSaveOrOpenBlob if the browser is IE', (done) => { + const navigatorAny: any = window.navigator; + + navigatorAny.__defineGetter__('msSaveOrOpenBlob', () => { + done(); + }); + + const blob = new Blob([''], { type: 'text/html' }); + service.downloadBlob(blob, 'test_ie'); + }); + }); +}); diff --git a/lib/core/services/download.service.ts b/lib/core/services/download.service.ts new file mode 100644 index 0000000000..22cb62e9d1 --- /dev/null +++ b/lib/core/services/download.service.ts @@ -0,0 +1,89 @@ +/*! + * @license + * Copyright 2019 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'; + +@Injectable({ + providedIn: 'root' +}) +export class DownloadService { + private saveData: Function; + + constructor() { + this.saveData = (function() { + const a = document.createElement('a'); + document.body.appendChild(a); + a.style.display = 'none'; + + return function(fileData, format, fileName) { + let blob = null; + + if (format === 'blob' || format === 'data') { + blob = new Blob([fileData], { type: 'octet/stream' }); + } + + if (format === 'object' || format === 'json') { + const json = JSON.stringify(fileData); + blob = new Blob([json], { type: 'octet/stream' }); + } + + if (blob) { + if ( + typeof window.navigator !== 'undefined' && + window.navigator.msSaveOrOpenBlob + ) { + navigator.msSaveOrOpenBlob(blob, fileName); + } else { + const 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 Content to download. + * @param fileName Name of the resulting file. + */ + 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 fileName Name of the resulting file. + */ + 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 fileName Name of the resulting file. + */ + downloadJSON(json: any, fileName: string): void { + this.saveData(json, 'json', fileName); + } +} diff --git a/lib/core/services/public-api.ts b/lib/core/services/public-api.ts index 60dae7989d..a336846312 100644 --- a/lib/core/services/public-api.ts +++ b/lib/core/services/public-api.ts @@ -56,3 +56,4 @@ export * from './download-zip.service'; export * from './lock.service'; export * from './automation.service'; export * from './automation.service'; +export * from './download.service'; diff --git a/lib/core/services/thumbnail.service.spec.ts b/lib/core/services/thumbnail.service.spec.ts index 1f32bbd363..3f3b9e1111 100644 --- a/lib/core/services/thumbnail.service.spec.ts +++ b/lib/core/services/thumbnail.service.spec.ts @@ -19,10 +19,12 @@ import { TestBed } from '@angular/core/testing'; import { ThumbnailService } from './thumbnail.service'; import { setupTestBed } from '../testing/setupTestBed'; import { CoreTestingModule } from '../testing/core.testing.module'; +import { AlfrescoApiService } from './alfresco-api.service'; describe('ThumbnailService', () => { let service: ThumbnailService; + let apiService: AlfrescoApiService; setupTestBed({ imports: [CoreTestingModule] @@ -30,6 +32,7 @@ describe('ThumbnailService', () => { beforeEach(() => { service = TestBed.get(ThumbnailService); + apiService = TestBed.get(AlfrescoApiService); }); it('should return the correct icon for a plain text file', () => { @@ -49,8 +52,8 @@ describe('ThumbnailService', () => { }); it('should return the thumbnail URL for a content item', () => { - spyOn(service.contentService, 'getDocumentThumbnailUrl').and.returnValue('/fake-thumbnail.png'); - expect(service.getDocumentThumbnailUrl({})).toContain('/fake-thumbnail.png'); + spyOn(apiService.contentApi, 'getDocumentThumbnailUrl').and.returnValue('/fake-thumbnail.png'); + expect(service.getDocumentThumbnailUrl('some-id')).toContain('/fake-thumbnail.png'); }); }); diff --git a/lib/core/services/thumbnail.service.ts b/lib/core/services/thumbnail.service.ts index eb7e181a33..eeaf914ecd 100644 --- a/lib/core/services/thumbnail.service.ts +++ b/lib/core/services/thumbnail.service.ts @@ -19,7 +19,8 @@ import { Injectable } from '@angular/core'; import { MatIconRegistry } from '@angular/material'; import { DomSanitizer } from '@angular/platform-browser'; -import { ContentService } from './content.service'; +import { AlfrescoApiService } from './alfresco-api.service'; +import { NodeEntry } from '@alfresco/js-api'; @Injectable({ providedIn: 'root' @@ -156,7 +157,7 @@ export class ThumbnailService { 'selected': './assets/images/ft_ic_selected.svg' }; - constructor(public contentService: ContentService, matIconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { + constructor(protected apiService: AlfrescoApiService, matIconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { Object.keys(this.mimeTypeIcons).forEach((key) => { const url = sanitizer.bypassSecurityTrustResourceUrl(this.mimeTypeIcons[key]); @@ -167,12 +168,25 @@ export class ThumbnailService { /** * Gets a thumbnail URL for the given document node. - * @param node Node to get URL for. + * @param node Node or Node ID to get URL for. * @returns URL string */ - public getDocumentThumbnailUrl(node: any): string { - const thumbnail = this.contentService.getDocumentThumbnailUrl(node); - return thumbnail || this.DEFAULT_ICON; + getDocumentThumbnailUrl(node: NodeEntry | string, attachment?: boolean, ticket?: string): string { + let resultUrl: string; + + if (node) { + let nodeId: string; + + if (typeof node === 'string') { + nodeId = node; + } else if (node.entry) { + nodeId = node.entry.id; + } + + resultUrl = this.apiService.contentApi.getDocumentThumbnailUrl(nodeId, attachment, ticket); + } + + return resultUrl || this.DEFAULT_ICON; } /** diff --git a/lib/core/userinfo/services/ecm-user.service.spec.ts b/lib/core/userinfo/services/ecm-user.service.spec.ts index 41160ecdc1..cfbb052937 100644 --- a/lib/core/userinfo/services/ecm-user.service.spec.ts +++ b/lib/core/userinfo/services/ecm-user.service.spec.ts @@ -98,16 +98,8 @@ describe('EcmUserService', () => { spyOn(contentService, 'getContentUrl').and.callThrough(); const urlRs = service.getUserProfileImage(undefined); - expect(urlRs).toBeUndefined(); + expect(urlRs).toBeNull(); expect(contentService.getContentUrl).not.toHaveBeenCalled(); }); - - it('should build the body for the content service', () => { - spyOn(contentService, 'getContentUrl').and.callThrough(); - const urlRs = service.getUserProfileImage('fake-avatar-id'); - - expect(urlRs).toBeDefined(); - expect(contentService.getContentUrl).toHaveBeenCalledWith({entry: {id: 'fake-avatar-id'}}); - }); }); }); diff --git a/lib/core/userinfo/services/ecm-user.service.ts b/lib/core/userinfo/services/ecm-user.service.ts index f51f1b0c3f..6b211bf830 100644 --- a/lib/core/userinfo/services/ecm-user.service.ts +++ b/lib/core/userinfo/services/ecm-user.service.ts @@ -62,11 +62,11 @@ export class EcmUserService { * @param avatarId Target avatar * @returns Image URL */ - getUserProfileImage(avatarId: string) { + getUserProfileImage(avatarId: string): string { if (avatarId) { - const nodeObj = {entry: {id: avatarId}}; - return this.contentService.getContentUrl(nodeObj); + return this.contentService.getContentUrl(avatarId); } + return null; } /**