diff --git a/demo-shell/src/app/components/file-view/file-view.component.html b/demo-shell/src/app/components/file-view/file-view.component.html index 667a6db885..e1e3851c39 100644 --- a/demo-shell/src/app/components/file-view/file-view.component.html +++ b/demo-shell/src/app/components/file-view/file-view.component.html @@ -4,8 +4,8 @@ - @@ -300,6 +300,7 @@ +
{ if (node && node.isFile) { - this.isCommentEnabled = this.contentServices.hasPermissions(node, PermissionsEnum.NOT_CONSUMER) || - this.contentServices.hasAllowableOperations(node, AllowableOperationsEnum.UPDATE); + this.isCommentEnabled = this.contentService.hasPermissions(node, PermissionsEnum.NOT_CONSUMER) || + this.contentService.hasAllowableOperations(node, AllowableOperationsEnum.UPDATE); this.nodeId = id; return; } @@ -90,6 +91,9 @@ export class FileViewComponent implements OnInit { }, () => this.router.navigate(['/files', id]) ); + } else{ + this.urlFile = this.contentService.createTrustedUrl(this.preview.content); + this.filename = this.preview.name; } }); } diff --git a/lib/core/src/lib/viewer/components/img-viewer.component.ts b/lib/core/src/lib/viewer/components/img-viewer.component.ts index 3f2d30b240..1f554b9769 100644 --- a/lib/core/src/lib/viewer/components/img-viewer.component.ts +++ b/lib/core/src/lib/viewer/components/img-viewer.component.ts @@ -26,7 +26,6 @@ import { EventEmitter, AfterViewInit, ViewChild, HostListener, OnDestroy } from '@angular/core'; import { AppConfigService } from '../../app-config/app-config.service'; -import { UrlService } from '../../services/url.service'; import Cropper from 'cropperjs'; @Component({ @@ -47,9 +46,6 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() urlFile: string; - @Input() - blobFile: Blob; - @Input() fileName: string; @@ -73,8 +69,7 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } constructor( - private appConfigService: AppConfigService, - private urlService: UrlService) { + private appConfigService: AppConfigService) { this.initializeScaling(); } @@ -138,12 +133,7 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges) { - const blobFile = changes['blobFile']; - if (blobFile && blobFile.currentValue) { - this.urlFile = this.urlService.createTrustedUrl(this.blobFile); - return; - } - if (!this.urlFile && !this.blobFile) { + if (!this.urlFile) { throw new Error('Attribute urlFile or blobFile is required'); } } diff --git a/lib/core/src/lib/viewer/components/media-player.component.ts b/lib/core/src/lib/viewer/components/media-player.component.ts index 1052adeecd..40f374b73a 100644 --- a/lib/core/src/lib/viewer/components/media-player.component.ts +++ b/lib/core/src/lib/viewer/components/media-player.component.ts @@ -16,7 +16,6 @@ */ import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation, Output, EventEmitter } from '@angular/core'; -import { ContentService } from '../../services/content.service'; import { Track } from '../models/viewer.model'; @Component({ @@ -31,9 +30,6 @@ export class MediaPlayerComponent implements OnChanges { @Input() urlFile: string; - @Input() - blobFile: Blob; - @Input() mimeType: string; @@ -47,18 +43,11 @@ export class MediaPlayerComponent implements OnChanges { @Output() error = new EventEmitter(); - constructor(private contentService: ContentService) { + constructor() { } ngOnChanges(changes: SimpleChanges) { - const blobFile = changes['blobFile']; - - if (blobFile && blobFile.currentValue) { - this.urlFile = this.contentService.createTrustedUrl(this.blobFile); - return; - } - - if (!this.urlFile && !this.blobFile) { + if (!this.urlFile) { throw new Error('Attribute urlFile or blobFile is required'); } } diff --git a/lib/core/src/lib/viewer/components/pdf-viewer.component.ts b/lib/core/src/lib/viewer/components/pdf-viewer.component.ts index 0a30ac33b5..d7a3b12cad 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer.component.ts +++ b/lib/core/src/lib/viewer/components/pdf-viewer.component.ts @@ -56,9 +56,6 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { @Input() urlFile: string; - @Input() - blobFile: Blob; - @Input() fileName: string; @@ -149,21 +146,6 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges) { - const blobFile = changes['blobFile']; - - if (blobFile && blobFile.currentValue) { - const reader = new FileReader(); - reader.onload = async () => { - const pdfSource: PDFSource = { - ...this.pdfjsDefaultOptions, - data: reader.result, - withCredentials: this.appConfigService.get('auth.withCredentials', undefined) - }; - this.executePdf(pdfSource); - }; - reader.readAsArrayBuffer(blobFile.currentValue); - } - const urlFile = changes['urlFile']; if (urlFile && urlFile.currentValue) { const pdfSource: PDFSource = { @@ -179,7 +161,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { this.executePdf(pdfSource); } - if (!this.urlFile && !this.blobFile) { + if (!this.urlFile) { throw new Error('Attribute urlFile or blobFile is required'); } } diff --git a/lib/core/src/lib/viewer/components/txt-viewer.component.ts b/lib/core/src/lib/viewer/components/txt-viewer.component.ts index 7caba710ad..268fd509e6 100644 --- a/lib/core/src/lib/viewer/components/txt-viewer.component.ts +++ b/lib/core/src/lib/viewer/components/txt-viewer.component.ts @@ -31,27 +31,18 @@ export class TxtViewerComponent implements OnChanges { @Input() urlFile: any; - @Input() - blobFile: Blob; - content: string | ArrayBuffer; constructor(private http: HttpClient, private appConfigService: AppConfigService) { } ngOnChanges(changes: SimpleChanges): Promise { - - const blobFile = changes['blobFile']; - if (blobFile && blobFile.currentValue) { - return this.readBlob(blobFile.currentValue); - } - const urlFile = changes['urlFile']; if (urlFile && urlFile.currentValue) { return this.getUrlContent(urlFile.currentValue); } - if (!this.urlFile && !this.blobFile) { + if (!this.urlFile) { throw new Error('Attribute urlFile or blobFile is required'); } @@ -71,20 +62,4 @@ export class TxtViewerComponent implements OnChanges { }); } - private readBlob(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = () => { - this.content = reader.result; - resolve(); - }; - - reader.onerror = (error: any) => { - reject(error); - }; - - reader.readAsText(blob); - }); - } } diff --git a/lib/core/src/lib/viewer/components/viewer-render.component.html b/lib/core/src/lib/viewer/components/viewer-render.component.html index 5f1f3fec92..a6168c368a 100644 --- a/lib/core/src/lib/viewer/components/viewer-render.component.html +++ b/lib/core/src/lib/viewer/components/viewer-render.component.html @@ -37,7 +37,6 @@ @@ -62,15 +60,13 @@ [urlFile]="urlFile" [tracks]="tracks" [mimeType]="mimeType" - [blobFile]="blobFile" [fileName]="internalFileName" (error)="onUnsupportedFile()"> - + diff --git a/lib/core/src/lib/viewer/components/viewer-render.component.ts b/lib/core/src/lib/viewer/components/viewer-render.component.ts index e77009942b..28c2b62c3b 100644 --- a/lib/core/src/lib/viewer/components/viewer-render.component.ts +++ b/lib/core/src/lib/viewer/components/viewer-render.component.ts @@ -42,10 +42,6 @@ export class ViewerRenderComponent implements OnChanges, OnInit, OnDestroy { @Input() urlFile = ''; - /** Loads a Blob File */ - @Input() - blobFile: Blob; - /** Toggles the 'Full Screen' feature. */ @Input() allowFullScreen = true; @@ -144,22 +140,12 @@ export class ViewerRenderComponent implements OnChanges, OnInit, OnDestroy { ngOnChanges() { this.isLoading = true; - if (this.blobFile) { - this.setUpBlobData(); - } else if (this.urlFile) { + if (this.urlFile) { this.setUpUrlFile(); } this.isLoading = false; } - private setUpBlobData() { - this.internalFileName = this.fileName; - this.internalViewerType = this.viewUtilService.getViewerTypeByMimeType(this.blobFile.type); - - this.extensionChange.emit(this.blobFile.type); - this.scrollTop(); - } - private setUpUrlFile() { this.internalFileName = this.fileName ? this.fileName : this.viewUtilService.getFilenameFromUrl(this.urlFile); this.extension = this.viewUtilService.getFileExtension(this.internalFileName); diff --git a/lib/core/src/lib/viewer/components/viewer.component.html b/lib/core/src/lib/viewer/components/viewer.component.html new file mode 100644 index 0000000000..3b7cde144b --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer.component.html @@ -0,0 +1,172 @@ +
+ +
+ + + + + + + + + + + + +
+ + + {{ fileName }} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + + + +
+
+
diff --git a/lib/core/src/lib/viewer/components/viewer.component.scss b/lib/core/src/lib/viewer/components/viewer.component.scss new file mode 100644 index 0000000000..9a8173d19a --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer.component.scss @@ -0,0 +1,77 @@ +/* stylelint-disable scss/at-extend-no-missing-placeholder */ +.adf-full-screen { + width: 100%; + height: 100%; + background-color: var(--theme-card-bg-color); +} + +.adf-alfresco-viewer { + position: absolute; + width: 100%; + height: 100%; + + .mat-toolbar { + color: var(--theme-text-color); + + .adf-toolbar-title { + width: auto; + } + } + + &-main { + width: 0; + } + + &__mimeicon { + vertical-align: middle; + height: 18px; + width: 18px; + } + + &-toolbar { + .mat-toolbar { + background-color: var(--theme-card-bg-bold-color); + } + } + + &__file-title { + text-align: center; + } + + &__display-name { + font-size: var(--theme-subheading-2-font-size); + opacity: 0.87; + line-height: 1.5; + letter-spacing: -0.4px; + font-weight: normal; + font-style: normal; + font-stretch: normal; + max-width: 400px; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + vertical-align: middle; + color: var(--theme-text-fg-color); + } + + &-inline-container { + @extend .adf-full-screen; + } + + &__sidebar { + width: 350px; + display: block; + padding: 0; + background-color: var(--theme-background-color); + box-shadow: 0 2px 4px 0 var(--theme-text-fg-shadow-color); + overflow: auto; + + &__right { + border-left: 1px solid var(--theme-border-color); + } + + &__left { + border-right: 1px solid var(--theme-border-color); + } + } +} diff --git a/lib/core/src/lib/viewer/components/viewer.component.spec.ts b/lib/core/src/lib/viewer/components/viewer.component.spec.ts new file mode 100644 index 0000000000..b20eac596e --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer.component.spec.ts @@ -0,0 +1,944 @@ +/*! + * @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 { Location } from '@angular/common'; +import { SpyLocation } from '@angular/common/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { TranslateModule } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { NodeEntry, VersionEntry } from '@alfresco/js-api'; +import { AlfrescoViewerComponent, RenditionViewerService } from '@alfresco/adf-content-services'; +import { + AlfrescoApiService, + CoreTestingModule, + setupTestBed, + EventMock, + FileModel, UploadService +} from '@alfresco/adf-core'; +import { throwError } from 'rxjs'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'adf-viewer-container-toolbar', + template: ` + + +
+
+
+ ` +}) +class ViewerWithCustomToolbarComponent { +} + +@Component({ + selector: 'adf-viewer-container-toolbar-actions', + template: ` + + + + + + ` +}) +class ViewerWithCustomToolbarActionsComponent { +} + +@Component({ + selector: 'adf-viewer-container-sidebar', + template: ` + + +
+
+
+ ` +}) +class ViewerWithCustomSidebarComponent { +} + +@Component({ + selector: 'adf-dialog-dummy', + template: `` +}) +class DummyDialogComponent { +} + +@Component({ + selector: 'adf-viewer-container-open-with', + template: ` + + + + + + + + ` +}) +class ViewerWithCustomOpenWithComponent { +} + +@Component({ + selector: 'adf-viewer-container-more-actions', + template: ` + + + + + + + + ` +}) +class ViewerWithCustomMoreActionsComponent { +} + + +describe('AlfrescoViewerComponent', () => { + + let component: AlfrescoViewerComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + let alfrescoApiService: AlfrescoApiService; + let dialog: MatDialog; + let uploadService: UploadService; + let extensionService: AppExtensionService; + + setupTestBed({ + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + CoreTestingModule, + MatButtonModule, + MatIconModule + ], + declarations: [ + ViewerWithCustomToolbarComponent, + ViewerWithCustomSidebarComponent, + ViewerWithCustomOpenWithComponent, + ViewerWithCustomMoreActionsComponent, + ViewerWithCustomToolbarActionsComponent + ], + providers: [ + { + provide: RenditionViewerService, useValue: { + getNodeRendition: () => throwError('thrown') + } + }, + {provide: Location, useClass: SpyLocation}, + MatDialog + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AlfrescoViewerComponent); + element = fixture.nativeElement; + component = fixture.componentInstance; + + uploadService = TestBed.inject(UploadService); + alfrescoApiService = TestBed.inject(AlfrescoApiService); + dialog = TestBed.inject(MatDialog); + extensionService = TestBed.inject(AppExtensionService); + }); + + afterEach(() => { + fixture.destroy(); + }); + + + describe('Extension Type Test', () => { + + + it('should use external viewer to display node by id', fakeAsync(() => { + const extension: ViewerExtensionRef = { + component: 'custom.component', + id: 'custom.component.id', + fileExtension: '*' + }; + spyOn(extensionService, 'getViewerExtensions').and.returnValue([extension]); + + fixture = TestBed.createComponent(AlfrescoViewerComponent); + element = fixture.nativeElement; + component = fixture.componentInstance; + + spyOn(component.nodesApi, 'getNode').and.callFake(() => Promise.resolve(new NodeEntry({entry: {}}))); + + component.nodeId = '37f7f34d-4e64-4db6-bb3f-5c89f7844251'; + component.ngOnChanges(); + + fixture.detectChanges(); + tick(100); + + expect(component.nodesApi.getNode).toHaveBeenCalled(); + expect(component.viewerType).toBe('external'); + expect(component.isLoading).toBeFalsy(); + expect(element.querySelector('[data-automation-id="custom.component"]')).not.toBeNull(); + })); + + + }); + + describe('MimeType handling', () => { + + it('should node without content show unkonwn', (done) => { + const displayName = 'the-name'; + const contentUrl = '/content/url/path'; + + component.nodeId = '12'; + spyOn(component['nodesApi'], 'getNode').and.returnValue(Promise.resolve(new NodeEntry({ + entry: {content: {name: displayName, id: '12'}} + }))); + + spyOn(component['contentApi'], 'getContentUrl').and.returnValue(contentUrl); + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('adf-viewer-unknown-format')).toBeDefined(); + done(); + }); + }); + }); + + it('should change display name every time node changes', fakeAsync(() => { + spyOn(component['nodesApi'], 'getNode').and.returnValues( + Promise.resolve(new NodeEntry({entry: {name: 'file1', content: {}}})), + Promise.resolve(new NodeEntry({entry: {name: 'file2', content: {}}})) + ); + + component.showViewer = true; + + component.nodeId = 'id1'; + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file1'); + + component.nodeId = 'id2'; + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file2'); + })); + + it('should append version of the file to the file content URL', fakeAsync(() => { + spyOn(component['nodesApi'], 'getNode').and.returnValue( + Promise.resolve(new NodeEntry({ + entry: { + name: 'file1.pdf', + content: {}, + properties: {'cm:versionLabel': '10'} + } + })) + ); + spyOn(component['versionsApi'], 'getVersion').and.returnValue(Promise.resolve(undefined)); + + component.nodeId = 'id1'; + component.showViewer = true; + + component.versionId = null; + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file1.pdf'); + expect(component.urlFileContent).toContain('/public/alfresco/versions/1/nodes/id1/content?attachment=false&10'); + })); + + it('should change display name every time node\`s version changes', fakeAsync(() => { + spyOn(component['nodesApi'], 'getNode').and.returnValue( + Promise.resolve(new NodeEntry({entry: {name: 'node1', content: {}}})) + ); + + spyOn(component['versionsApi'], 'getVersion').and.returnValues( + Promise.resolve(new VersionEntry({entry: {name: 'file1', content: {}}})), + Promise.resolve(new VersionEntry({entry: {name: 'file2', content: {}}})) + ); + + component.nodeId = 'id1'; + component.showViewer = true; + + component.versionId = '1.0'; + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file1'); + + component.versionId = '1.1'; + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file2'); + })); + + it('should update node only if node name changed', fakeAsync(() => { + spyOn(component['nodesApi'], 'getNode').and.returnValues( + Promise.resolve(new NodeEntry({entry: {name: 'file1', content: {}}})) + ); + + component.showViewer = true; + + component.nodeId = 'id1'; + fixture.detectChanges(); + component.ngOnChanges(); + tick(); + + expect(component.fileName).toBe('file1'); + + alfrescoApiService.nodeUpdated.next({id: 'id1', name: 'file2'} as any); + fixture.detectChanges(); + expect(component.fileName).toBe('file2'); + + alfrescoApiService.nodeUpdated.next({id: 'id1', name: 'file3'} as any); + fixture.detectChanges(); + expect(component.fileName).toBe('file3'); + + alfrescoApiService.nodeUpdated.next({id: 'id2', name: 'file4'} as any); + fixture.detectChanges(); + expect(component.fileName).toBe('file3'); + expect(component.nodeId).toBe('id1'); + })); + + describe('Viewer Example Component Rendering', () => { + + it('should use custom toolbar', (done) => { + const customFixture = TestBed.createComponent(ViewerWithCustomToolbarComponent); + const customElement: HTMLElement = customFixture.nativeElement; + + customFixture.detectChanges(); + fixture.whenStable().then(() => { + expect(customElement.querySelector('.custom-toolbar-element')).toBeDefined(); + done(); + }); + }); + + it('should use custom toolbar actions', (done) => { + const customFixture = TestBed.createComponent(ViewerWithCustomToolbarActionsComponent); + const customElement: HTMLElement = customFixture.nativeElement; + + customFixture.detectChanges(); + fixture.whenStable().then(() => { + expect(customElement.querySelector('#custom-button')).toBeDefined(); + done(); + }); + }); + + it('should use custom info drawer', (done) => { + const customFixture = TestBed.createComponent(ViewerWithCustomSidebarComponent); + const customElement: HTMLElement = customFixture.nativeElement; + + customFixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(customElement.querySelector('.custom-info-drawer-element')).toBeDefined(); + done(); + }); + }); + + it('should use custom open with menu', (done) => { + const customFixture = TestBed.createComponent(ViewerWithCustomOpenWithComponent); + const customElement: HTMLElement = customFixture.nativeElement; + + customFixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(customElement.querySelector('.adf-viewer-container-open-with')).toBeDefined(); + done(); + }); + }); + + it('should use custom more actions menu', (done) => { + const customFixture = TestBed.createComponent(ViewerWithCustomMoreActionsComponent); + const customElement: HTMLElement = customFixture.nativeElement; + + customFixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(customElement.querySelector('.adf-viewer-container-more-actions')).toBeDefined(); + done(); + }); + + }); + }); + + describe('error handling', () => { + + it('should show unknown view when node file not found', (done) => { + spyOn(component['nodesApi'], 'getNode') + .and.returnValue(Promise.reject({})); + + component.nodeId = 'the-node-id-of-the-file-to-preview'; + component.mimeType = null; + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('adf-viewer-unknown-format')).not.toBeNull(); + done(); + }); + }); + + it('should show unknown view when sharedLink file not found', (done) => { + spyOn(component['sharedLinksApi'], 'getSharedLink') + .and.returnValue(Promise.reject({})); + + component.sharedLinkId = 'the-Shared-Link-id'; + component.mimeType = null; + component.nodeId = null; + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('adf-viewer-unknown-format')).not.toBeNull(); + done(); + }); + + }); + + it('should raise an event when the shared link is invalid', fakeAsync(() => { + spyOn(component['sharedLinksApi'], 'getSharedLink') + .and.returnValue(Promise.reject({})); + + component.sharedLinkId = 'the-Shared-Link-id'; + component.mimeType = null; + component.nodeId = null; + + component.invalidSharedLink.subscribe((emittedValue) => { + expect(emittedValue).toBeUndefined(); + }); + + component.ngOnChanges(); + })); +// + }); + + describe('Toolbar', () => { + + it('should show only next file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = false; + component.canNavigateNext = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).not.toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should provide tooltip for next file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = false; + component.canNavigateNext = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton.title).toBe('ADF_VIEWER.ACTIONS.NEXT_FILE'); + }); + + it('should show only previous file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).not.toBeNull(); + }); + + it('should provide tooltip for the previous file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton.title).toBe('ADF_VIEWER.ACTIONS.PREV_FILE'); + }); + + it('should show both file navigation buttons', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).not.toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).not.toBeNull(); + }); + + it('should not show navigation buttons', async () => { + component.allowNavigate = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should now show navigation buttons even if navigation enabled', async () => { + component.allowNavigate = true; + component.canNavigateBefore = false; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should render fullscreen button', () => { + expect(element.querySelector('[data-automation-id="adf-toolbar-fullscreen"]')).toBeDefined(); + }); + + it('should render default download button', (done) => { + component.allowDownload = true; + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-download"]')).toBeDefined(); + done(); + }); + }); + + it('should not render default download button', (done) => { + component.allowDownload = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-download"]')).toBeNull(); + done(); + }); + }); + + it('should render default print button', (done) => { + component.allowPrint = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-print"]')).toBeDefined(); + done(); + }); + }); + + it('should not render default print button', (done) => { + component.allowPrint = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-print"]')).toBeNull(); + done(); + }); + }); + + it('should invoke print action with the toolbar button', (done) => { + component.allowPrint = true; + fixture.detectChanges(); + + spyOn(component, 'onPrintContent').and.stub(); + + const button: HTMLButtonElement = element.querySelector('[data-automation-id="adf-toolbar-print"]') as HTMLButtonElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.onPrintContent).toHaveBeenCalled(); + done(); + }); + }); + + it('should get and assign node for download', (done) => { + component.nodeId = '12'; + const displayName = 'the-name'; + const nodeDetails = { + entry: {name: displayName, id: '12', content: {mimeType: 'txt'}} + }; + + const contentUrl = '/content/url/path'; + + const node = new NodeEntry(nodeDetails); + + spyOn(component['nodesApi'], 'getNode').and.returnValue(Promise.resolve(node)); + spyOn(component['contentApi'], 'getContentUrl').and.returnValue(contentUrl); + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.nodeEntry).toBe(node); + done(); + }); + }); + + it('should render close viewer button if it is not a shared link', (done) => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).toBeDefined(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).not.toBeNull(); + done(); + }); + }); + + it('should not render close viewer button if it is a shared link', (done) => { + spyOn(component['sharedLinksApi'], 'getSharedLink') + .and.returnValue(Promise.reject({})); + + component.sharedLinkId = 'the-Shared-Link-id'; + component.mimeType = null; + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).toBeNull(); + done(); + }); + }); + + }); + + describe('Base component', () => { + + beforeEach(() => { + component.mimeType = 'application/pdf'; + component.nodeId = 'id1'; + + fixture.detectChanges(); + }); + + describe('SideBar Test', () => { + + it('should NOT display sidebar if is not allowed', (done) => { + component.showRightSidebar = true; + component.allowRightSidebar = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-right-sidebar'); + expect(sidebar).toBeNull(); + done(); + }); + }); + + it('should display sidebar on the right side', (done) => { + component.allowRightSidebar = true; + component.showRightSidebar = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-right-sidebar'); + expect(getComputedStyle(sidebar).order).toEqual('4'); + done(); + }); + }); + + it('should NOT display left sidebar if is not allowed', (done) => { + component.showLeftSidebar = true; + component.allowLeftSidebar = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-left-sidebar'); + expect(sidebar).toBeNull(); + done(); + }); + + }); + + it('should display sidebar on the left side', (done) => { + component.allowLeftSidebar = true; + component.showLeftSidebar = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-left-sidebar'); + expect(getComputedStyle(sidebar).order).toEqual('1'); + done(); + }); + }); + }); + + describe('View', () => { + + describe('Overlay mode true', () => { + + beforeEach(() => { + component.overlayMode = true; + component.fileName = 'fake-test-file.pdf'; + fixture.detectChanges(); + }); + + it('should header be present if is overlay mode', () => { + expect(element.querySelector('.adf-alfresco-viewer-toolbar')).not.toBeNull(); + }); + + it('should Name File be present if is overlay mode ', (done) => { + component.ngOnChanges(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('#adf-alfresco-viewer-display-name').textContent).toEqual('fake-test-file.pdf'); + done(); + }); + }); + + it('should Close button be present if overlay mode', (done) => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('.adf-alfresco-viewer-close-button')).not.toBeNull(); + done(); + }); + }); + + it('should Click on close button hide the viewer', (done) => { + const closeButton: any = element.querySelector('.adf-alfresco-viewer-close-button'); + closeButton.click(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('.adf-alfresco-viewer-content')).toBeNull(); + done(); + }); + }); + + it('should Esc button hide the viewer', (done) => { + EventMock.keyDown(27); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('.adf-alfresco-viewer-content')).toBeNull(); + done(); + }); + }); + + it('should not close the viewer on Escape event if dialog was opened', (done) => { + const event = new KeyboardEvent('keydown', { + bubbles: true, + keyCode: 27 + } as KeyboardEventInit); + + const dialogRef = dialog.open(DummyDialogComponent); + + dialogRef.afterClosed().subscribe(() => { + EventMock.keyDown(27); + fixture.detectChanges(); + expect(element.querySelector('.adf-alfresco-viewer-content')).toBeNull(); + done(); + }); + + fixture.detectChanges(); + + document.body.dispatchEvent(event); + fixture.detectChanges(); + expect(element.querySelector('.adf-alfresco-viewer-content')).not.toBeNull(); + }); + }); + + describe('Overlay mode false', () => { + + beforeEach(() => { + component.overlayMode = false; + fixture.detectChanges(); + }); + + it('should Esc button not hide the viewer if is not overlay mode', (done) => { + EventMock.keyDown(27); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('.adf-alfresco-viewer-content')).not.toBeNull(); + done(); + }); + }); + }); + }); + + describe('Attribute', () => { + + it('should FileNodeId present not thrown any error ', () => { + component.showViewer = true; + component.nodeId = 'file-node-id'; + + expect(() => { + component.ngOnChanges(); + }).not.toThrow(); + }); + + + it('should showViewer default value be true', () => { + expect(component.showViewer).toBe(true); + }); + + it('should viewer be hide if showViewer value is false', () => { + component.showViewer = false; + + fixture.detectChanges(); + expect(element.querySelector('.adf-alfresco-viewer-content')).toBeNull(); + }); + }); + + describe('Events', () => { + + it('should update version when emitted by image-viewer and user has update permissions', () => { + spyOn(uploadService, 'uploadFilesInTheQueue').and.callFake(() => { + }); + spyOn(uploadService, 'addToQueue'); + component.readOnly = false; + component.nodeEntry = new NodeEntry({ + entry: { + name: 'fakeImage.png', + id: '12', + content: {mimeType: 'img/png'} + } + }); + const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); + const fakeBlob = new Blob([data], {type: 'image/png'}); + const newImageFile: File = new File([fakeBlob], component?.nodeEntry?.entry?.name, {type: component?.nodeEntry?.entry?.content?.mimeType}); + const newFile = new FileModel( + newImageFile, + { + majorVersion: false, + newVersion: true, + parentId: component?.nodeEntry?.entry?.parentId, + nodeType: component?.nodeEntry?.entry?.content?.mimeType + }, + component.nodeEntry.entry?.id + ); + component.onSubmitFile(fakeBlob); + fixture.detectChanges(); + + expect(uploadService.addToQueue).toHaveBeenCalledWith(...[newFile]); + expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalled(); + }); + + it('should not update version when emitted by image-viewer and user doesn`t have update permissions', () => { + spyOn(uploadService, 'uploadFilesInTheQueue').and.callFake(() => { + }); + component.readOnly = true; + component.nodeEntry = new NodeEntry({ + entry: { + name: 'fakeImage.png', + id: '12', + content: {mimeType: 'img/png'} + } + }); + const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); + const fakeBlob = new Blob([data], {type: 'image/png'}); + component.onSubmitFile(fakeBlob); + fixture.detectChanges(); + + expect(uploadService.uploadFilesInTheQueue).not.toHaveBeenCalled(); + }); + }); + + describe('Viewer component - Full Screen Mode - Mocking fixture element', () => { + + beforeEach(() => { + fixture = TestBed.createComponent(AlfrescoViewerComponent); + element = fixture.nativeElement; + component = fixture.componentInstance; + + component.showToolbar = true; + component.nodeId = 'fake-node-id'; + component.mimeType = 'application/pdf'; + fixture.detectChanges(); + }); + + it('should use standard mode', () => { + const domElement = jasmine.createSpyObj('el', ['requestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.requestFullscreen).toHaveBeenCalled(); + }); + + it('should use webkit prefix', () => { + const domElement = jasmine.createSpyObj('el', ['webkitRequestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.webkitRequestFullscreen).toHaveBeenCalled(); + }); + + it('should use moz prefix', () => { + const domElement = jasmine.createSpyObj('el', ['mozRequestFullScreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.mozRequestFullScreen).toHaveBeenCalled(); + }); + + it('should use ms prefix', () => { + const domElement = jasmine.createSpyObj('el', ['msRequestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.msRequestFullscreen).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/lib/core/src/lib/viewer/components/viewer.component.ts b/lib/core/src/lib/viewer/components/viewer.component.ts new file mode 100644 index 0000000000..04cae341b1 --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer.component.ts @@ -0,0 +1,233 @@ +/*! + * @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 { + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { ViewUtilService } from '../services/view-util.service'; +import { ViewerToolbarComponent } from './viewer-toolbar.component'; +import { ViewerOpenWithComponent } from './viewer-open-with.component'; +import { ViewerMoreActionsComponent } from './viewer-more-actions.component'; +import { ViewerSidebarComponent } from "./viewer-sidebar.component"; + +@Component({ + selector: 'adf-viewer', + templateUrl: './alfresco-viewer.component.html', + styleUrls: ['./alfresco-viewer.component.scss'], + host: {class: 'adf-alfresco-viewer'}, + encapsulation: ViewEncapsulation.None, + providers: [ViewUtilService] +}) +export class ViewerComponent implements OnInit, OnDestroy { + + @ContentChild(ViewerToolbarComponent) + toolbar: ViewerToolbarComponent; + + @ContentChild(ViewerSidebarComponent) + sidebar: ViewerSidebarComponent; + + @ContentChild(ViewerOpenWithComponent) + mnuOpenWith: ViewerOpenWithComponent; + + @ContentChild(ViewerMoreActionsComponent) + mnuMoreActions: ViewerMoreActionsComponent; + + /** If you want to load an external file that does not come from ACS you + * can use this URL to specify where to load the file from. + */ + @Input() + urlFile = ''; + + /** Override Content filename. */ + @Input() + fileName: string; + + /** Hide or show the viewer */ + @Input() + showViewer = true; + + /** Allows `back` navigation */ + @Input() + allowGoBack = true; + + /** Hide or show the toolbar */ + @Input() + showToolbar = true; + + /** If `true` then show the Viewer as a full page over the current content. + * Otherwise fit inside the parent div. + */ + @Input() + overlayMode = false; + + /** Toggles before/next navigation. You can use the arrow buttons to navigate + * between documents in the collection. + */ + @Input() + allowNavigate = false; + + /** Toggles the "before" ("<") button. Requires `allowNavigate` to be enabled. */ + @Input() + canNavigateBefore = true; + + /** Toggles the next (">") button. Requires `allowNavigate` to be enabled. */ + @Input() + canNavigateNext = true; + + /** Allow the left the sidebar. */ + @Input() + allowLeftSidebar = false; + + /** Allow the right sidebar. */ + @Input() + allowRightSidebar = false; + + /** Toggles right sidebar visibility. Requires `allowRightSidebar` to be set to `true`. */ + @Input() + showRightSidebar = false; + + /** Toggles left sidebar visibility. Requires `allowLeftSidebar` to be set to `true`. */ + @Input() + showLeftSidebar = false; + + /** The template for the right sidebar. The template context contains the loaded node data. */ + @Input() + sidebarRightTemplate: TemplateRef = null; + + /** The template for the left sidebar. The template context contains the loaded node data. */ + @Input() + sidebarLeftTemplate: TemplateRef = null; + + /** Emitted when the shared link used is not valid. */ + @Output() + invalidSharedLink = new EventEmitter(); + + /** Emitted when user clicks 'Navigate Before' ("<") button. */ + @Output() + navigateBefore = new EventEmitter(); + + /** Emitted when user clicks 'Navigate Next' (">") button. */ + @Output() + navigateNext = new EventEmitter(); + + /** Emitted when the viewer close */ + @Output() + close = new EventEmitter(); + + private onDestroy$ = new Subject(); + + isLoading: boolean; + urlFileContent: string; + viewerType: any; + mimeType: string; + readOnly: boolean = true; + + sidebarRightTemplateContext: { node: Node } = {node: null}; + sidebarLeftTemplateContext: { node: Node } = {node: null}; + + constructor(private el: ElementRef, + public dialog: MatDialog) { + } + + + onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) { + this.navigateBefore.next(event); + } + + onNavigateNextClick(event: MouseEvent | KeyboardEvent) { + this.navigateNext.next(event); + } + + /** + * close the viewer + */ + onClose() { + this.showViewer = false; + this.close.emit(this.showViewer); + } + + toggleRightSidebar() { + this.showRightSidebar = !this.showRightSidebar; + } + + toggleLeftSidebar() { + this.showLeftSidebar = !this.showLeftSidebar; + } + + @HostListener('document:keyup', ['$event']) + handleKeyboardEvent(event: KeyboardEvent) { + if (event && event.defaultPrevented) { + return; + } + + const key = event.keyCode; + + // Left arrow + if (key === 37 && this.canNavigateBefore) { + event.preventDefault(); + this.onNavigateBeforeClick(event); + } + + // Right arrow + if (key === 39 && this.canNavigateNext) { + event.preventDefault(); + this.onNavigateNextClick(event); + } + + // Ctrl+F + if (key === 70 && event.ctrlKey) { + event.preventDefault(); + this.enterFullScreen(); + } + } + + /** + * Triggers full screen mode with a main content area displayed. + */ + enterFullScreen(): void { + const container = this.el.nativeElement.querySelector('.adf-viewer__fullscreen-container'); + if (container) { + if (container.requestFullscreen) { + container.requestFullscreen(); + } else if (container.webkitRequestFullscreen) { + container.webkitRequestFullscreen(); + } else if (container.mozRequestFullScreen) { + container.mozRequestFullScreen(); + } else if (container.msRequestFullscreen) { + container.msRequestFullscreen(); + } + } + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + +} diff --git a/lib/core/src/lib/viewer/public-api.ts b/lib/core/src/lib/viewer/public-api.ts index a32a35ea9d..566f9b632a 100644 --- a/lib/core/src/lib/viewer/public-api.ts +++ b/lib/core/src/lib/viewer/public-api.ts @@ -31,6 +31,7 @@ export * from './components/viewer-sidebar.component'; export * from './components/viewer-toolbar.component'; export * from './components/viewer-toolbar-actions.component'; export * from './components/viewer-render.component'; +export * from './components/viewer.component'; export * from './directives/viewer-extension.directive'; diff --git a/lib/core/src/lib/viewer/viewer.module.ts b/lib/core/src/lib/viewer/viewer.module.ts index 01d8dbe2d1..dbb7984f45 100644 --- a/lib/core/src/lib/viewer/viewer.module.ts +++ b/lib/core/src/lib/viewer/viewer.module.ts @@ -43,6 +43,7 @@ import { ViewerExtensionDirective } from './directives/viewer-extension.directiv import { ViewerToolbarActionsComponent } from './components/viewer-toolbar-actions.component'; import { DirectiveModule } from '../directives/directive.module'; import { A11yModule } from '@angular/cdk/a11y'; +import { ViewerComponent } from "./components/viewer.component"; @NgModule({ imports: [ @@ -73,7 +74,8 @@ import { A11yModule } from '@angular/cdk/a11y'; ViewerSidebarComponent, ViewerOpenWithComponent, ViewerMoreActionsComponent, - ViewerToolbarActionsComponent + ViewerToolbarActionsComponent, + ViewerComponent ], exports: [ ViewerRenderComponent, @@ -90,7 +92,8 @@ import { A11yModule } from '@angular/cdk/a11y'; ViewerSidebarComponent, ViewerOpenWithComponent, ViewerMoreActionsComponent, - ViewerToolbarActionsComponent + ViewerToolbarActionsComponent, + ViewerComponent ] }) export class ViewerModule {