[AAE-10778] Refactor Viewer (#7992)

* refactor version 1 many todo

* split render from viewer
move alfresco render in content pack

* refactor part 2

* test fixed

* fix doc

* [AAE-10778] Fix lint issues

* [AAE-10778] Fix lint issue: remove duplicated declaration

* [AAE-10778] Fix lint issue: use flex shorthand rule

* [AAE-10778] Fix FormService and WidgetComponent imports

* [AAE-10778] Fix import FormModel, FormService, FormFieldModel from adf-core

* [AAE-10778] Implement missing oninit, onchanges and ondestroy

* [AAE-10778] Replace adf-viewer with adf-alfresco-viewer, update escape command to close the viewer

* [AAE-10778] Fix unit test: fix the class name to match the 'adf-viewer-render.image-viewer-scaling' get from the appConfigService

* [AAE-10778] Fix image-viewer unit tests: replace ContentService with UrlService

* [AAE-10778] Fix unit test 'should if the extension change extension Change event be fired': emit file extension when the filename extension change

* [AAE-10778] Fix unit test: expect for internalFileName value instead of display-name id because the display name logic has been moved to the alfresco-viewer.component

* [AAE-10778] Fix unit test: remove display name it because the unknown display name value is no longer handled after refactoring

* [AAE-10778] Fix e2e: [C260096] Should the Viewer able to accept a customToolbar

* [AAE-10778] Update selector to fix e2e: '[C362265] Should the Viewer be able to download a previous version of a file'

* [AAE-10778] Update selector to fix e2e: '[C260038] Should display first page, toolbar and pagination when opening a .pdf file'

* fix aftrer rebase

* fix unit test

* [AAE-10778] Add adf viewer component that is node agnostic, show adf-alfresco-viewer or adf-viewer into file-view-component if blob or node are set

* [AAE-10778] Update viewer export path

* [AAE-10778] Update selectors since have been updated in the viewer component

* [AAE-10778] Call adf-viewer from alfresco-viewer, project adf-alfresco-viewer content to adf-viewer

* [AAE-10778] Remove full screen unit tests from alfresco-viewer component becase that logic is handled in the viewer.component

* [AAE-10778] Export toolbar custom actions component

* [AAE-10778] Pass mimeType as input to adf-viewer to update mime icon

* [AAE-10778] Remove e2e because the custom name behaviour has been removed from the file-view.component (9f21b6dc69\#diff-4b438dc59784dce9eb7634cfeca6d8db61362966343bd3d6895a3edafdf4cfd5L129)

* [AAE-10778] Use two-way binding for showViewer change to fix C260100

* [AAE-10778] Update prefix css selectors to adf-viewer because are related to the adf-viewer component

* [AAE-10778] Update prefix css selectors to adf-viewer in the unit tests because are related to the adf-viewer component

* [AAE-10778] Update the output name to showViewerChange to navigate to primary url after closing the viewer

* [AAE-10778] Pass right and left sidebar template context to viewer component (fix C362242)

* [AAE-10778] Add allowFullScreen input to disable/enable full screen behaviour

* [AAE-10778] Handle loading visualization only inside the viewer-render component

* [AAE-10778] PDF viewer: fix mat-progress-bar is not showed during the pdf loading, center progress bar

* [AAE-10778] Remove isLoading from unit tests because no longer exists

* [AAE-10778] Remove viewerType input from adf-viewer, viewerType will be handled by viewer-render

* [AAE-10778] Remove console.log

* [AAE-10778] Remove check full screen button is not displayed on the media file because is not needed anymore, we don't need to check for the fullscreen button in the viewer component

* [AAE-10778] Check for node rendtion before to assign to urlFileContent and mimeType

* [AAE-10778] Process Services Cloud: register file-viewer widget that uses adf-alfresco-viewer component to display content from ACS

* [AAE-10778] Core: rename file-viewer widget into base-viewer, base-viewer no longer accept nodeId, but will accept urlFile and blobFile

* [AAE-10778] Process Services: register file-viewer widget that uses adf-alfresco-viewer component to display content from ACS

* [AAE-10778] Base viewer widget: show viewer only if there's a file input

* [AAE-10778] Viewer component: check for fileName when urlFile is provided as Input

* [AAE-10778] Viewer component documentation

* [AAE-10778] Update upgrade guide with viewer changes

* [AAE-10778] Fix double quote lint issue after rebase

---------

Co-authored-by: Amedeo Lepore <amedeo.lepore@hyland.com>
Co-authored-by: Amedeo Lepore <amedeo.lepore85@gmail.com>
This commit is contained in:
Eugenio Romano
2023-02-01 17:25:43 +01:00
committed by GitHub
parent 52520bb61e
commit 4043d55fc4
85 changed files with 4803 additions and 2729 deletions

View File

@@ -48,6 +48,7 @@ import { ContentPipeModule } from './pipes/content-pipe.module';
import { NodeCommentsModule } from './node-comments/node-comments.module';
import { TreeModule } from './tree/tree.module';
import { SearchTextModule } from './search-text/search-text-input.module';
import { AlfrescoViewerModule } from './viewer/alfresco-viewer.module';
@NgModule({
imports: [
@@ -79,7 +80,8 @@ import { SearchTextModule } from './search-text/search-text-input.module';
VersionCompatibilityModule,
NodeCommentsModule,
TreeModule,
SearchTextModule
SearchTextModule,
AlfrescoViewerModule
],
providers: [
{
@@ -115,7 +117,8 @@ import { SearchTextModule } from './search-text/search-text-input.module';
VersionCompatibilityModule,
NodeCommentsModule,
TreeModule,
SearchTextModule
SearchTextModule,
AlfrescoViewerModule
]
})
export class ContentModule {

View File

@@ -0,0 +1,51 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { MaterialModule } from '../material.module';
import { A11yModule } from '@angular/cdk/a11y';
import { AlfrescoViewerComponent } from './components/alfresco-viewer.component';
import { CoreModule } from '@alfresco/adf-core';
@NgModule({
imports: [
CoreModule,
CommonModule,
MaterialModule,
TranslateModule,
FormsModule,
ReactiveFormsModule,
FlexLayoutModule,
A11yModule,
ExtensionsModule
],
declarations: [
AlfrescoViewerComponent
],
exports: [
AlfrescoViewerComponent
]
})
export class AlfrescoViewerModule {
}

View File

@@ -0,0 +1,71 @@
<adf-viewer
#adfViewer
[(showViewer)]="showViewer"
[allowGoBack]="allowGoBack"
[showToolbar]="showToolbar"
[overlayMode]="overlayMode"
[allowNavigate]="allowNavigate"
[canNavigateBefore]="canNavigateBefore"
[canNavigateNext]="canNavigateNext"
[allowLeftSidebar]="allowLeftSidebar"
[allowRightSidebar]="allowRightSidebar"
[showRightSidebar]="showRightSidebar"
[showLeftSidebar]="showLeftSidebar"
[allowFullScreen]="allowFullScreen"
[sidebarRightTemplate]="sidebarRightTemplate"
[sidebarLeftTemplate]="sidebarLeftTemplate"
[sidebarRightTemplateContext]="sidebarRightTemplateContext"
[sidebarLeftTemplateContext]="sidebarLeftTemplateContext"
[fileName]="fileName"
[mimeType]="mimeType"
[urlFile]="urlFileContent"
[tracks]="tracks"
[readOnly]="readOnly"
(navigateBefore)="onNavigateBeforeClick($event)"
(navigateNext)="onNavigateNextClick($event)"
(showViewerChange)="onClose()"
(submitFile)="onSubmitFile($event)">
<adf-viewer-toolbar *ngIf="toolbar">
<ng-content select="adf-viewer-toolbar"></ng-content>
</adf-viewer-toolbar>
<adf-viewer-toolbar-actions *ngIf="toolbarActions">
<ng-content select="adf-viewer-toolbar-actions"></ng-content>
</adf-viewer-toolbar-actions>
<adf-viewer-more-actions *ngIf="moreActions">
<ng-content select="adf-viewer-more-actions"></ng-content>
</adf-viewer-more-actions>
<adf-viewer-open-with *ngIf="openWith">
<ng-content select="adf-viewer-open-with"></ng-content>
</adf-viewer-open-with>
<adf-viewer-sidebar *ngIf="sidebar">
<ng-content select="adf-viewer-sidebar"></ng-content>
</adf-viewer-sidebar>
<adf-viewer-toolbar-custom-actions>
<button id="adf-alfresco-viewer-download"
*ngIf="allowDownload"
mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate }}"
data-automation-id="adf-toolbar-download"
[adfNodeDownload]="nodeEntry"
[version]="versionEntry">
<mat-icon>file_download</mat-icon>
</button>
<button id="adf-alfresco-viewer-print"
*ngIf="allowPrint"
mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ACTIONS.PRINT' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.PRINT' | translate }}"
data-automation-id="adf-toolbar-print"
(click)="onPrintContent($event)">
<mat-icon>print</mat-icon>
</button>
</adf-viewer-toolbar-custom-actions>
</adf-viewer>

View File

@@ -0,0 +1 @@
/* stylelint-disable-next-line no-empty-source */

View File

@@ -0,0 +1,907 @@
/*!
* @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 {
NodesApiService,
CoreTestingModule,
setupTestBed,
EventMock,
FileModel, UploadService, ViewUtilService
} from '@alfresco/adf-core';
import { throwError } from 'rxjs';
import { Component } from '@angular/core';
@Component({
selector: 'adf-viewer-container-toolbar',
template: `
<adf-alfresco-viewer>
<adf-viewer-toolbar>
<div class="custom-toolbar-element"></div>
</adf-viewer-toolbar>
</adf-alfresco-viewer>
`
})
class ViewerWithCustomToolbarComponent {
}
@Component({
selector: 'adf-viewer-container-toolbar-actions',
template: `
<adf-alfresco-viewer>
<adf-viewer-toolbar-actions>
<button mat-icon-button id="custom-button">
<mat-icon>alarm</mat-icon>
</button>
</adf-viewer-toolbar-actions>
</adf-alfresco-viewer>
`
})
class ViewerWithCustomToolbarActionsComponent {
}
@Component({
selector: 'adf-viewer-container-sidebar',
template: `
<adf-alfresco-viewer>
<adf-viewer-sidebar>
<div class="custom-sidebar"></div>
</adf-viewer-sidebar>
</adf-alfresco-viewer>
`
})
class ViewerWithCustomSidebarComponent {
}
@Component({
selector: 'adf-dialog-dummy',
template: ``
})
class DummyDialogComponent {
}
@Component({
selector: 'adf-viewer-container-open-with',
template: `
<adf-alfresco-viewer>
<adf-viewer-open-with>
<button mat-menu-item>
<mat-icon>dialpad</mat-icon>
<span>Option 1</span>
</button>
<button mat-menu-item disabled>
<mat-icon>voicemail</mat-icon>
<span>Option 2</span>
</button>
<button mat-menu-item>
<mat-icon>notifications_off</mat-icon>
<span>Option 3</span>
</button>
</adf-viewer-open-with>
</adf-alfresco-viewer>
`
})
class ViewerWithCustomOpenWithComponent {
}
@Component({
selector: 'adf-viewer-container-more-actions',
template: `
<adf-alfresco-viewer>
<adf-viewer-more-actions>
<button mat-menu-item>
<mat-icon>dialpad</mat-icon>
<span>Action One</span>
</button>
<button mat-menu-item disabled>
<mat-icon>voicemail</mat-icon>
<span>Action Two</span>
</button>
<button mat-menu-item>
<mat-icon>notifications_off</mat-icon>
<span>Action Three</span>
</button>
</adf-viewer-more-actions>
</adf-alfresco-viewer>
`
})
class ViewerWithCustomMoreActionsComponent {
}
describe('AlfrescoViewerComponent', () => {
let component: AlfrescoViewerComponent;
let fixture: ComponentFixture<AlfrescoViewerComponent>;
let element: HTMLElement;
let nodesApiService: NodesApiService;
let dialog: MatDialog;
let uploadService: UploadService;
let extensionService: AppExtensionService;
let renditionService: RenditionViewerService;
let viewUtilService: ViewUtilService;
setupTestBed({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
CoreTestingModule,
MatButtonModule,
MatIconModule
],
declarations: [
ViewerWithCustomToolbarComponent,
ViewerWithCustomSidebarComponent,
ViewerWithCustomOpenWithComponent,
ViewerWithCustomMoreActionsComponent,
ViewerWithCustomToolbarActionsComponent
],
providers: [
{
provide: RenditionViewerService, useValue: {
getNodeRendition: () => throwError('thrown'),
generateMediaTracksRendition: () => {}
}
},
{provide: Location, useClass: SpyLocation},
MatDialog
]
});
beforeEach(() => {
fixture = TestBed.createComponent(AlfrescoViewerComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
uploadService = TestBed.inject(UploadService);
nodesApiService = TestBed.inject(NodesApiService);
dialog = TestBed.inject(MatDialog);
extensionService = TestBed.inject(AppExtensionService);
renditionService = TestBed.inject(RenditionViewerService);
viewUtilService = TestBed.inject(ViewUtilService);
});
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]);
spyOn(renditionService, 'getNodeRendition');
spyOn(renditionService, 'generateMediaTracksRendition');
spyOn(viewUtilService, 'getViewerType').and.returnValue('external');
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(renditionService.getNodeRendition).not.toHaveBeenCalled();
expect(renditionService.generateMediaTracksRendition).not.toHaveBeenCalled();
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');
nodesApiService.nodeUpdated.next({id: 'id1', name: 'file2'} as any);
fixture.detectChanges();
expect(component.fileName).toBe('file2');
nodesApiService.nodeUpdated.next({id: 'id1', name: 'file3'} as any);
fixture.detectChanges();
expect(component.fileName).toBe('file3');
nodesApiService.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<HTMLButtonElement>('[data-automation-id="adf-toolbar-next-file"]');
expect(nextButton).not.toBeNull();
const prevButton = element.querySelector<HTMLButtonElement>('[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<HTMLButtonElement>('[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<HTMLButtonElement>('[data-automation-id="adf-toolbar-next-file"]');
expect(nextButton).toBeNull();
const prevButton = element.querySelector<HTMLButtonElement>('[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<HTMLButtonElement>('[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<HTMLButtonElement>('[data-automation-id="adf-toolbar-next-file"]');
expect(nextButton).not.toBeNull();
const prevButton = element.querySelector<HTMLButtonElement>('[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<HTMLButtonElement>('[data-automation-id="adf-toolbar-next-file"]');
expect(nextButton).toBeNull();
const prevButton = element.querySelector<HTMLButtonElement>('[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<HTMLButtonElement>('[data-automation-id="adf-toolbar-next-file"]');
expect(nextButton).toBeNull();
const prevButton = element.querySelector<HTMLButtonElement>('[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-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-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-viewer-close-button')).not.toBeNull();
done();
});
});
it('should Click on close button hide the viewer', (done) => {
const closeButton: any = element.querySelector('.adf-viewer-close-button');
closeButton.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('.adf-viewer-content')).toBeNull();
done();
});
});
it('should Esc button hide the viewer', (done) => {
EventMock.keyDown(27);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('.adf-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-viewer-content')).toBeNull();
done();
});
fixture.detectChanges();
document.body.dispatchEvent(event);
fixture.detectChanges();
expect(element.querySelector('.adf-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-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-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();
});
});
});
});

View File

@@ -0,0 +1,451 @@
/*!
* @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 {
ChangeDetectorRef,
Component,
ContentChild,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
AlfrescoApiService, ContentService,
FileModel,
LogService,
NodesApiService,
Track,
UploadService,
ViewerComponent,
ViewerMoreActionsComponent,
ViewerOpenWithComponent,
ViewerSidebarComponent,
ViewerToolbarActionsComponent,
ViewerToolbarComponent,
ViewUtilService
} from '@alfresco/adf-core';
import { Subject } from 'rxjs';
import {
ContentApi,
Node,
NodeEntry,
NodesApi,
RenditionEntry,
SharedlinksApi,
Version,
VersionEntry,
VersionsApi
} from '@alfresco/js-api';
import { RenditionViewerService } from '../services/rendition-viewer.service';
import { MatDialog } from '@angular/material/dialog';
import { filter, takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-alfresco-viewer',
templateUrl: './alfresco-viewer.component.html',
styleUrls: ['./alfresco-viewer.component.scss'],
host: {class: 'adf-alfresco-viewer'},
encapsulation: ViewEncapsulation.None,
providers: [ViewUtilService]
})
export class AlfrescoViewerComponent implements OnChanges, OnInit, OnDestroy {
@ViewChild('adfViewer')
adfViewer: ViewerComponent<{node: Node}>;
@ContentChild(ViewerToolbarComponent)
toolbar: ViewerToolbarComponent;
@ContentChild(ViewerSidebarComponent)
sidebar: ViewerSidebarComponent;
@ContentChild(ViewerToolbarActionsComponent)
toolbarActions: ViewerToolbarActionsComponent;
@ContentChild(ViewerMoreActionsComponent)
moreActions: ViewerMoreActionsComponent;
@ContentChild(ViewerOpenWithComponent)
openWith: ViewerOpenWithComponent;
/** Node Id of the file to load. */
@Input()
nodeId: string = null;
/** Version Id of the file to load. */
@Input()
versionId: string = null;
/** Shared link id (to display shared file). */
@Input()
sharedLinkId: string = null;
/** Hide or show the viewer */
@Input()
showViewer = true;
/** Number of times the Viewer will retry fetching content Rendition.
* There is a delay of at least one second between attempts.
*/
@Input()
maxRetries = 30;
/** 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;
/** Toggles downloading. */
@Input()
allowDownload = true;
/** Toggles printing. */
@Input()
allowPrint = false;
/** Toggles the 'Full Screen' feature. */
@Input()
allowFullScreen = true;
/** The template for the right sidebar. The template context contains the loaded node data. */
@Input()
sidebarRightTemplate: TemplateRef<any> = null;
/** The template for the left sidebar. The template context contains the loaded node data. */
@Input()
sidebarLeftTemplate: TemplateRef<any> = 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<MouseEvent | KeyboardEvent>();
/** Emitted when user clicks 'Navigate Next' (">") button. */
@Output()
navigateNext = new EventEmitter<MouseEvent | KeyboardEvent>();
/** Emitted when the viewer close */
@Output()
showViewerChange = new EventEmitter<boolean>();
private onDestroy$ = new Subject<boolean>();
private cacheBusterNumber: number;
versionEntry: VersionEntry;
urlFileContent: string;
fileName: string;
mimeType: string;
nodeEntry: NodeEntry;
tracks: Track[] = [];
readOnly: boolean = true;
sidebarRightTemplateContext: { node: Node } = {node: null};
sidebarLeftTemplateContext: { node: Node } = {node: null};
_sharedLinksApi: SharedlinksApi;
get sharedLinksApi(): SharedlinksApi {
this._sharedLinksApi = this._sharedLinksApi ?? new SharedlinksApi(this.apiService.getInstance());
return this._sharedLinksApi;
}
_versionsApi: VersionsApi;
get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.apiService.getInstance());
return this._versionsApi;
}
_nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance());
return this._nodesApi;
}
_contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
constructor(private apiService: AlfrescoApiService,
private nodesApiService: NodesApiService,
private renditionViewerService: RenditionViewerService,
private viewUtilService: ViewUtilService,
private logService: LogService,
private contentService: ContentService,
private uploadService: UploadService,
public dialog: MatDialog,
private cdr: ChangeDetectorRef) {
renditionViewerService.maxRetries = this.maxRetries;
}
ngOnInit() {
this.nodesApiService.nodeUpdated.pipe(
filter((node) => node && node.id === this.nodeId &&
(node.name !== this.fileName ||
this.getNodeVersionProperty(this.nodeEntry.entry) !== this.getNodeVersionProperty(node))),
takeUntil(this.onDestroy$)
).subscribe((node) => this.onNodeUpdated(node));
}
private async onNodeUpdated(node: Node) {
if (node && node.id === this.nodeId) {
this.generateCacheBusterNumber();
await this.setUpNodeFile(node);
}
}
private getNodeVersionProperty(node: Node): string {
return node?.properties['cm:versionLabel'] ?? '';
}
private async setupSharedLink() {
this.allowGoBack = false;
try {
const sharedLinkEntry = await this.sharedLinksApi.getSharedLink(this.sharedLinkId);
await this.setUpSharedLinkFile(sharedLinkEntry);
} catch (error) {
this.logService.error('This sharedLink does not exist');
this.invalidSharedLink.next();
this.mimeType = 'invalid-link';
this.urlFileContent = 'invalid-file';
}
}
private async setupNode() {
try {
this.nodeEntry = await this.nodesApi.getNode(this.nodeId, {include: ['allowableOperations']});
if (this.versionId) {
this.versionEntry = await this.versionsApi.getVersion(this.nodeId, this.versionId);
await this.setUpNodeFile(this.nodeEntry.entry, this.versionEntry.entry);
} else {
await this.setUpNodeFile(this.nodeEntry.entry);
this.cdr.detectChanges();
}
} catch (error) {
this.urlFileContent = 'invalid-node';
this.logService.error('This node does not exist');
}
}
private async setUpNodeFile(nodeData: Node, versionData?: Version): Promise<void> {
this.readOnly = !this.contentService.hasAllowableOperations(nodeData, 'update');
let mimeType;
let urlFileContent;
if (versionData && versionData.content) {
mimeType = versionData.content.mimeType;
} else if (nodeData.content) {
mimeType = nodeData.content.mimeType;
}
const currentFileVersion = this.nodeEntry?.entry?.properties && this.nodeEntry.entry.properties['cm:versionLabel'] ?
encodeURI(this.nodeEntry?.entry?.properties['cm:versionLabel']) : encodeURI('1.0');
urlFileContent = versionData ? this.contentApi.getVersionContentUrl(this.nodeId, versionData.id) :
this.contentApi.getContentUrl(this.nodeId);
urlFileContent = this.cacheBusterNumber ? urlFileContent + '&' + currentFileVersion + '&' + this.cacheBusterNumber :
urlFileContent + '&' + currentFileVersion;
const fileExtension = this.viewUtilService.getFileExtension(versionData ? versionData.name : nodeData.name);
this.fileName = versionData ? versionData.name : nodeData.name;
const viewerType = this.viewUtilService.getViewerType(fileExtension, mimeType);
if (viewerType === 'unknown') {
let nodeRendition;
if (versionData) {
nodeRendition = await this.renditionViewerService.getNodeRendition(nodeData.id, versionData.id);
} else {
nodeRendition = await this.renditionViewerService.getNodeRendition(nodeData.id);
}
if(nodeRendition){
urlFileContent = nodeRendition.url;
mimeType = nodeRendition.mimeType;
}
} else if (viewerType === 'media') {
this.tracks = await this.renditionViewerService.generateMediaTracksRendition(this.nodeId);
}
this.mimeType = mimeType;
this.urlFileContent = urlFileContent;
this.sidebarRightTemplateContext.node = nodeData;
this.sidebarLeftTemplateContext.node = nodeData;
}
private async setUpSharedLinkFile(details: any) {
let mimeType = details.entry.content.mimeType;
const fileExtension = this.viewUtilService.getFileExtension(details.entry.name);
this.fileName = details.entry.name;
let urlFileContent = this.contentApi.getSharedLinkContentUrl(this.sharedLinkId, false);
const viewerType = this.viewUtilService.getViewerType(fileExtension, mimeType);
if (viewerType === 'unknown') {
({
url: urlFileContent,
mimeType
} = await this.getSharedLinkRendition(this.sharedLinkId));
}
this.mimeType = mimeType;
this.urlFileContent = urlFileContent;
}
private async getSharedLinkRendition(sharedId: string): Promise<{ url: string; mimeType: string }> {
try {
const rendition: RenditionEntry = await this.sharedLinksApi.getSharedLinkRendition(sharedId, 'pdf');
if (rendition.entry.status.toString() === 'CREATED') {
const urlFileContent = this.contentApi.getSharedLinkRenditionUrl(sharedId, 'pdf');
return {url: urlFileContent, mimeType: 'application/pdf'};
}
} catch (error) {
this.logService.error(error);
try {
const rendition: RenditionEntry = await this.sharedLinksApi.getSharedLinkRendition(sharedId, 'imgpreview');
if (rendition.entry.status.toString() === 'CREATED') {
const urlFileContent = this.contentApi.getSharedLinkRenditionUrl(sharedId, 'imgpreview');
return {url: urlFileContent, mimeType: 'image/png'};
}
} catch (renditionError) {
this.logService.error(renditionError);
return null;
}
}
return null;
}
private generateCacheBusterNumber() {
this.cacheBusterNumber = Date.now();
}
/**
* close the viewer
*/
onClose() {
this.showViewerChange.emit(this.showViewer);
}
onPrintContent(event: MouseEvent) {
if (this.allowPrint) {
if (!event.defaultPrevented) {
this.renditionViewerService.printFileGeneric(this.nodeId, this.mimeType);
}
}
}
onSubmitFile(newImageBlob: Blob) {
if (this?.nodeEntry?.entry?.id && !this.readOnly) {
const newImageFile: File = new File([newImageBlob], this?.nodeEntry?.entry?.name, {type: this?.nodeEntry?.entry?.content?.mimeType});
const newFile = new FileModel(
newImageFile,
{
majorVersion: false,
newVersion: true,
parentId: this?.nodeEntry?.entry?.parentId,
nodeType: this?.nodeEntry?.entry?.content?.mimeType
},
this?.nodeEntry?.entry?.id
);
this.uploadService.addToQueue(...[newFile]);
this.uploadService.uploadFilesInTheQueue();
}
}
onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) {
this.navigateBefore.next(event);
}
onNavigateNextClick(event: MouseEvent | KeyboardEvent) {
this.navigateNext.next(event);
}
isSourceDefined(): boolean {
return !!(this.nodeId || this.sharedLinkId);
}
ngOnChanges() {
if (this.showViewer) {
if (!this.isSourceDefined()) {
throw new Error('A content source attribute value is missing.');
}
if (this.nodeId) {
this.setupNode();
} else if (this.sharedLinkId) {
this.setupSharedLink();
}
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './services/rendition-viewer.service';
export * from './components/alfresco-viewer.component';
export * from './alfresco-viewer.module';

View File

@@ -0,0 +1,306 @@
/*!
* @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';
import { ContentApi, RenditionEntry, RenditionPaging, RenditionsApi, VersionsApi } from '@alfresco/js-api';
import { AlfrescoApiService , LogService, Track,TranslationService, ViewUtilService } from '@alfresco/adf-core';
@Injectable({
providedIn: 'root'
})
export class RenditionViewerService {
static TARGET = '_new';
/**
* Content groups based on categorization of files that can be viewed in the web browser. This
* implementation or grouping is tied to the definition the ng component: ViewerRenderComponent
*/
static ContentGroup = {
IMAGE: 'image',
MEDIA: 'media',
PDF: 'pdf',
TEXT: 'text'
};
/**
* The name of the rendition with the media subtitles in the supported format
*/
static SUBTITLES_RENDITION_NAME = 'webvtt';
/**
* Based on ViewerRenderComponent Implementation, this value is used to determine how many times we try
* to get the rendition of a file for preview, or printing.
*/
maxRetries = 5;
/**
* Timeout used for setInterval.
*/
private TRY_TIMEOUT: number = 10000;
_renditionsApi: RenditionsApi;
get renditionsApi(): RenditionsApi {
this._renditionsApi = this._renditionsApi ?? new RenditionsApi(this.apiService.getInstance());
return this._renditionsApi;
}
_contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
_versionsApi: VersionsApi;
private DEFAULT_RENDITION: string = 'imgpreview';
get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.apiService.getInstance());
return this._versionsApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService,
private translateService: TranslationService,
private viewUtilsService: ViewUtilService) {
}
getRenditionUrl(nodeId: string, type: string, renditionExists: boolean): string {
return (renditionExists && type !== RenditionViewerService.ContentGroup.IMAGE) ?
this.contentApi.getRenditionUrl(nodeId, RenditionViewerService.ContentGroup.PDF) :
this.contentApi.getContentUrl(nodeId, false);
}
private async waitRendition(nodeId: string, renditionId: string, retries: number): Promise<RenditionEntry> {
const rendition = await this.renditionsApi.getRendition(nodeId, renditionId);
if (this.maxRetries < retries) {
const status = rendition.entry.status.toString();
if (status === 'CREATED') {
return rendition;
} else {
retries += 1;
await this.wait(1000);
return this.waitRendition(nodeId, renditionId, retries);
}
}
return Promise.resolve(null);
}
private wait(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async getRendition(nodeId: string, renditionId: string): Promise<RenditionEntry> {
const renditionPaging: RenditionPaging = await this.renditionsApi.listRenditions(nodeId);
let rendition: RenditionEntry = renditionPaging.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
if (rendition) {
const status = rendition.entry.status.toString();
if (status === 'NOT_CREATED') {
try {
await this.renditionsApi.createRendition(nodeId, {id: renditionId});
rendition = await this.waitRendition(nodeId, renditionId, 0);
} catch (err) {
this.logService.error(err);
}
}
}
return new Promise<RenditionEntry>((resolve) => resolve(rendition));
}
async getNodeRendition(nodeId: string, versionId?: string): Promise<{ url: string; mimeType: string }> {
try {
return versionId ? await this.resolveNodeRendition(nodeId, 'pdf', versionId) :
await this.resolveNodeRendition(nodeId, 'pdf');
} catch (err) {
this.logService.error(err);
return null;
}
}
private async resolveNodeRendition(nodeId: string, renditionId: string, versionId?: string): Promise<{ url: string; mimeType: string }> {
renditionId = renditionId.toLowerCase();
const supportedRendition: RenditionPaging = versionId ? await this.versionsApi.listVersionRenditions(nodeId, versionId) :
await this.renditionsApi.listRenditions(nodeId);
let rendition = this.findRenditionById(supportedRendition, renditionId);
if (!rendition) {
renditionId = this.DEFAULT_RENDITION;
rendition = this.findRenditionById(supportedRendition, this.DEFAULT_RENDITION);
}
if (rendition) {
const status: string = rendition.entry.status.toString();
const mimeType: string = rendition.entry.content.mimeType;
if (status === 'NOT_CREATED') {
return {url: await this.requestCreateRendition(nodeId, renditionId, versionId), mimeType:mimeType};
} else {
return {url: await this.handleNodeRendition(nodeId, renditionId, versionId), mimeType:mimeType};
}
}
return null;
}
private async requestCreateRendition(nodeId: string, renditionId: string, versionId: string): Promise<string> {
try {
if (versionId) {
await this.versionsApi.createVersionRendition(nodeId, versionId, {id: renditionId});
} else {
await this.renditionsApi.createRendition(nodeId, {id: renditionId});
}
try {
return versionId ? await this.waitNodeRendition(nodeId, renditionId, versionId) : await this.waitNodeRendition(nodeId, renditionId);
} catch (e) {
return null;
}
} catch (err) {
this.logService.error(err);
return null;
}
}
private findRenditionById(supportedRendition: RenditionPaging, renditionId: string) {
const rendition: RenditionEntry = supportedRendition.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
return rendition;
}
private async waitNodeRendition(nodeId: string, renditionId: string, versionId?: string): Promise<string> {
let currentRetry: number = 0;
return new Promise<string>((resolve, reject) => {
const intervalId = setInterval(() => {
currentRetry++;
if (this.maxRetries >= currentRetry) {
if (versionId) {
this.versionsApi.getVersionRendition(nodeId, versionId, renditionId).then((rendition: RenditionEntry) => {
const status: string = rendition.entry.status.toString();
if (status === 'CREATED') {
clearInterval(intervalId);
return resolve(this.handleNodeRendition(nodeId, rendition.entry.content.mimeType, versionId));
}
}, () => reject());
} else {
this.renditionsApi.getRendition(nodeId, renditionId).then((rendition: RenditionEntry) => {
const status: string = rendition.entry.status.toString();
if (status === 'CREATED') {
clearInterval(intervalId);
return resolve(this.handleNodeRendition(nodeId, renditionId, versionId));
}
}, () => reject());
}
} else {
clearInterval(intervalId);
return reject();
}
}, this.TRY_TIMEOUT);
});
}
private async handleNodeRendition(nodeId: string, renditionId: string, versionId?: string): Promise<string> {
const url = versionId ? this.contentApi.getVersionRenditionUrl(nodeId, versionId, renditionId) :
this.contentApi.getRenditionUrl(nodeId, renditionId);
return url;
}
async generateMediaTracksRendition(nodeId: string): Promise<Track[]> {
return this.isRenditionAvailable(nodeId, RenditionViewerService.SUBTITLES_RENDITION_NAME)
.then((value) => {
const tracks = [];
if (value) {
tracks.push({
kind: 'subtitles',
src: this.contentApi.getRenditionUrl(nodeId, RenditionViewerService.SUBTITLES_RENDITION_NAME),
label: this.translateService.instant('ADF_VIEWER.SUBTITLES')
});
}
return tracks;
})
.catch((err) => {
this.logService.error('Error while retrieving ' + RenditionViewerService.SUBTITLES_RENDITION_NAME + ' rendition');
this.logService.error(err);
return [];
});
}
private async isRenditionAvailable(nodeId: string, renditionId: string): Promise<boolean> {
const renditionPaging: RenditionPaging = await this.renditionsApi.listRenditions(nodeId);
const rendition: RenditionEntry = renditionPaging.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
return rendition?.entry?.status?.toString() === 'CREATED' || false;
}
/**
* This method takes a url to trigger the print dialog against, and the type of artifact that it
* is.
* This URL should be one that can be rendered in the browser, for example PDF, Image, or Text
*/
printFile(url: string, type: string): void {
const pwa = window.open(url, RenditionViewerService.TARGET);
if (pwa) {
// Because of the way chrome focus and close image window vs. pdf preview window
if (type === RenditionViewerService.ContentGroup.IMAGE) {
pwa.onfocus = () => {
setTimeout(() => {
pwa.close();
}, 500);
};
}
pwa.onload = () => {
pwa.print();
};
}
}
/**
* Launch the File Print dialog from anywhere other than the preview service, which resolves the
* rendition of the object that can be printed from a web browser.
* These are: images, PDF files, or PDF rendition of files.
* We also force PDF rendition for TEXT type objects, otherwise the default URL is to download.
* TODO there are different TEXT type objects, (HTML, plaintext, xml, etc. we should determine how these are handled)
*/
printFileGeneric(objectId: string, mimeType: string): void {
const nodeId = objectId;
const type: string = this.viewUtilsService.getViewerTypeByMimeType(mimeType);
this.getRendition(nodeId, RenditionViewerService.ContentGroup.PDF)
.then((value) => {
const url: string = this.getRenditionUrl(nodeId, type, (!!value));
const printType = (type === RenditionViewerService.ContentGroup.PDF
|| type === RenditionViewerService.ContentGroup.TEXT)
? RenditionViewerService.ContentGroup.PDF : type;
this.printFile(url, printType);
})
.catch((err) => {
this.logService.error('Error with Printing');
this.logService.error(err);
});
}
}

View File

@@ -44,5 +44,6 @@ export * from './lib/common/index';
export * from './lib/tree/index';
export * from './lib/category/index';
export * from './lib/search-text/index';
export * from './lib/viewer/index';
export * from './lib/content.module';

View File

@@ -0,0 +1,7 @@
<div class="adf-base-viewer-widget {{field.className}}" [class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<adf-viewer [urlFile]="field.value.urlFile" [blobFile]="field.value.blobFile" [fileName]="field.value.fileName" [showViewer]="field.value?.blobFile || field.value?.urlFile" [allowGoBack]="false"></adf-viewer>
<error-widget [error]="field.validationSummary"></error-widget>
</div>

View File

@@ -0,0 +1,19 @@
base-viewer-widget {
height: 100%;
width: 100%;
.adf-base-viewer-widget {
height: 100%;
width: 100%;
adf-viewer.adf-viewer {
position: relative;
.adf-viewer-container {
.adf-viewer-content > div {
height: 90vh;
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
/*!
* @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 { FormModel } from '../core/form.model';
import { TranslateModule } from '@ngx-translate/core';
import { FormFieldModel } from '../core/form-field.model';
import { FormService } from '../../../services/form.service';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { BaseViewerWidgetComponent } from './base-viewer.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('BaseViewerWidgetComponent', () => {
const fakeForm = new FormModel();
let widget: BaseViewerWidgetComponent;
let formServiceStub: Partial<FormService>;
let fixture: ComponentFixture<BaseViewerWidgetComponent>;
const fakePngAnswer: any = {
id: '1933',
link: false,
isExternal: false,
relatedContent: false,
contentAvailable: true,
name: 'a_png_file.png',
simpleType: 'image',
mimeType: 'image/png',
previewStatus: 'queued',
thumbnailStatus: 'queued',
created: '2022-10-14T17:17:37.099Z',
createdBy: { id: 1001, firstName: 'Admin', lastName: 'admin', email: 'admin@example.com' }
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CoreTestingModule,
TranslateModule.forRoot()
],
declarations: [ BaseViewerWidgetComponent ],
providers: [ { provide: FormService, useValue: formServiceStub } ]
});
formServiceStub = TestBed.inject(FormService);
fixture = TestBed.createComponent(BaseViewerWidgetComponent);
widget = fixture.componentInstance;
});
it('should set the file id corretly when the field value is an array', (done) => {
const fakeField = new FormFieldModel(fakeForm, { id: 'fakeField', value: [fakePngAnswer] });
widget.field = fakeField;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(widget.field.value).toBe('1933');
done();
});
});
it('should set the file id corretly when the field value is a string', (done) => {
const fakeField = new FormFieldModel(fakeForm, { id: 'fakeField', value: 'fakeValue' });
widget.field = fakeField;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(widget.field.value).toBe('fakeValue');
done();
});
});
});

View File

@@ -0,0 +1,55 @@
/*!
* @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, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component';
/* eslint-disable @angular-eslint/component-selector */
@Component({
selector: 'base-viewer-widget',
templateUrl: './base-viewer.widget.html',
styleUrls: ['./base-viewer.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class BaseViewerWidgetComponent extends WidgetComponent implements OnInit {
constructor(formService: FormService) {
super(formService);
}
ngOnInit(): void {
if (this.field &&
this.field.value &&
Array.isArray(this.field.value) &&
this.field.value.length) {
const file = this.field.value[0];
this.field.value = file.id;
}
}
}

View File

@@ -39,7 +39,9 @@ export class FormFieldTypes {
static DOCUMENT: string = 'document';
static DATETIME: string = 'datetime';
static ATTACH_FOLDER: string = 'select-folder';
static FILE_VIEWER: string = 'file-viewer';
static PROPERTIES_VIEWER: string = 'properties-viewer';
static ALFRESCO_FILE_VIEWER: string = 'file-viewer';
static VIEWER: string = 'base-viewer';
static READONLY_TYPES: string[] = [
FormFieldTypes.HYPERLINK,

View File

@@ -29,7 +29,7 @@ import { InputMaskDirective } from './text/text-mask.component';
import { TextWidgetComponent } from './text/text.widget';
import { DateTimeWidgetComponent } from './date-time/date-time.widget';
import { JsonWidgetComponent } from './json/json.widget';
import { FileViewerWidgetComponent } from './file-viewer/file-viewer.widget';
import { BaseViewerWidgetComponent } from './base-viewer/base-viewer.widget';
import { DisplayRichTextWidgetComponent } from './display-rich-text/display-rich-text.widget';
// core
@@ -49,7 +49,7 @@ export * from './amount/amount.widget';
export * from './error/error.component';
export * from './date-time/date-time.widget';
export * from './json/json.widget';
export * from './file-viewer/file-viewer.widget';
export * from './base-viewer/base-viewer.widget';
export * from './display-rich-text/display-rich-text.widget';
export * from './text/text-mask.component';
@@ -66,7 +66,7 @@ export const WIDGET_DIRECTIVES: any[] = [
ErrorWidgetComponent,
DateTimeWidgetComponent,
JsonWidgetComponent,
FileViewerWidgetComponent,
BaseViewerWidgetComponent,
DisplayRichTextWidgetComponent
];

View File

@@ -39,7 +39,7 @@ export class FormRenderingService extends DynamicComponentMapper {
json: DynamicComponentResolver.fromType(widgets.JsonWidgetComponent),
readonly: DynamicComponentResolver.fromType(widgets.TextWidgetComponent),
datetime: DynamicComponentResolver.fromType(widgets.DateTimeWidgetComponent),
'file-viewer': DynamicComponentResolver.fromType(widgets.FileViewerWidgetComponent),
'base-viewer': DynamicComponentResolver.fromType(widgets.BaseViewerWidgetComponent),
'display-rich-text': DynamicComponentResolver.fromType(widgets.DisplayRichTextWidgetComponent)
};
}

View File

@@ -0,0 +1,42 @@
/*!
* @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';
import { DomSanitizer } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class UrlService {
constructor(private sanitizer: DomSanitizer) {
}
/**
* 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 Data to wrap into object URL
* @returns URL string
*/
createTrustedUrl(blob: Blob): string {
const url = window.URL.createObjectURL(blob);
return this.sanitizer.bypassSecurityTrustUrl(url) as string;
}
}

View File

@@ -1,5 +1,5 @@
<div id="adf-image-container" (keydown)="onKeyDown($event)" class="adf-image-container" tabindex="0" role="img" [attr.aria-label]="nameFile" data-automation-id="adf-image-container">
<img #image id="viewer-image" [src]="urlFile" [alt]="nameFile" (error)="onImageError()" />
<div id="adf-image-container" (keydown)="onKeyDown($event)" class="adf-image-container" tabindex="0" role="img" [attr.aria-label]="fileName" data-automation-id="adf-image-container">
<img #image id="viewer-image" [src]="urlFile" [alt]="fileName" (error)="onImageError()" />
</div>
<div class="adf-image-viewer__toolbar" *ngIf="showToolbar">
@@ -13,7 +13,7 @@
<mat-icon>zoom_out</mat-icon>
</button>
<div class="adf-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
<div class="adf-image-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
{{ currentScaleText }}
</div>

View File

@@ -48,5 +48,17 @@
display: inline-block;
margin-left: 10px;
}
&-page-scale {
cursor: default;
width: 79px;
height: 24px;
font-size: var(--theme-body-1-font-size);
border: 1px solid var(--theme-border-color);
text-align: center;
line-height: 24px;
margin-left: 4px;
margin-right: 4px;
}
}
}

View File

@@ -17,7 +17,7 @@
import { SimpleChange } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ContentService } from '../../services/content.service';
import { UrlService } from '../../services/url.service';
import { ImgViewerComponent } from './img-viewer.component';
import { setupTestBed, CoreTestingModule } from '../../testing';
import { AppConfigService } from '../../app-config/app-config.service';
@@ -27,7 +27,7 @@ import { By } from '@angular/platform-browser';
describe('Test Img viewer component ', () => {
let component: ImgViewerComponent;
let service: ContentService;
let urlService: UrlService;
let fixture: ComponentFixture<ImgViewerComponent>;
let element: HTMLElement;
@@ -46,7 +46,7 @@ describe('Test Img viewer component ', () => {
describe('Zoom customization', () => {
beforeEach(() => {
service = TestBed.inject(ContentService);
urlService = TestBed.inject(UrlService);
fixture = TestBed.createComponent(ImgViewerComponent);
element = fixture.nativeElement;
@@ -68,7 +68,7 @@ describe('Test Img viewer component ', () => {
beforeEach(() => {
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config['adf-viewer.image-viewer-scaling'] = 70;
appConfig.config['adf-viewer-render.image-viewer-scaling'] = 70;
component.initializeScaling();
});
@@ -86,7 +86,7 @@ describe('Test Img viewer component ', () => {
describe('Url', () => {
beforeEach(() => {
service = TestBed.inject(ContentService);
urlService = TestBed.inject(UrlService);
fixture = TestBed.createComponent(ImgViewerComponent);
element = fixture.nativeElement;
@@ -115,7 +115,7 @@ describe('Test Img viewer component ', () => {
describe('Blob', () => {
beforeEach(() => {
service = TestBed.inject(ContentService);
urlService = TestBed.inject(UrlService);
fixture = TestBed.createComponent(ImgViewerComponent);
element = fixture.nativeElement;
@@ -138,7 +138,7 @@ describe('Test Img viewer component ', () => {
});
it('The file Name should be present in the alt attribute', () => {
component.nameFile = 'fake-name';
component.fileName = 'fake-name';
fixture.detectChanges();
expect(element.querySelector('#viewer-image').getAttribute('alt')).toEqual('fake-name');
});
@@ -146,7 +146,7 @@ describe('Test Img viewer component ', () => {
it('If blob is passed should not thrown an error', () => {
const blob = createFakeBlob();
spyOn(service, 'createTrustedUrl').and.returnValue('fake-blob-url');
spyOn(urlService, 'createTrustedUrl').and.returnValue('fake-blob-url');
const change = new SimpleChange(null, blob, true);
expect(() => {
component.ngOnChanges({ blobFile: change });

View File

@@ -25,8 +25,8 @@ import {
Output,
EventEmitter, AfterViewInit, ViewChild, HostListener, OnDestroy
} from '@angular/core';
import { ContentService } from '../../services/content.service';
import { AppConfigService } from '../../app-config/app-config.service';
import { UrlService } from '../../services/url.service';
import Cropper from 'cropperjs';
@Component({
@@ -51,7 +51,7 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
blobFile: Blob;
@Input()
nameFile: string;
fileName: string;
// eslint-disable-next-line @angular-eslint/no-output-native
@Output()
@@ -74,12 +74,12 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
constructor(
private appConfigService: AppConfigService,
private contentService: ContentService) {
private urlService: UrlService) {
this.initializeScaling();
}
initializeScaling() {
const scaling = this.appConfigService.get<number>('adf-viewer.image-viewer-scaling', undefined) / 100;
const scaling = this.appConfigService.get<number>('adf-viewer-render.image-viewer-scaling', undefined) / 100;
if (scaling) {
this.scale = scaling;
}
@@ -140,7 +140,7 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) {
const blobFile = changes['blobFile'];
if (blobFile && blobFile.currentValue) {
this.urlFile = this.contentService.createTrustedUrl(this.blobFile);
this.urlFile = this.urlService.createTrustedUrl(this.blobFile);
return;
}
if (!this.urlFile && !this.blobFile) {

View File

@@ -1,4 +1,4 @@
<video controls [ngClass]="{'adf-audio-file': mimeType && mimeType.startsWith('audio')}">
<source [src]="urlFile" [type]="mimeType" (error)="onMediaPlayerError()"/>
<source [src]="urlFile" [type]="mimeType" (error)="onMediaPlayerError($event)"/>
<track *ngFor="let track of tracks" [kind]="track.kind" [label]="track.label" [srclang]="track.srclang" [src]="track.src"/>
</video>

View File

@@ -18,13 +18,12 @@
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation, Output, EventEmitter } from '@angular/core';
import { ContentService } from '../../services/content.service';
import { Track } from '../models/viewer.model';
import { ViewUtilService } from '../services/view-util.service';
@Component({
selector: 'adf-media-player',
templateUrl: './media-player.component.html',
styleUrls: ['./media-player.component.scss'],
host: { class: 'adf-media-player' },
host: {class: 'adf-media-player'},
encapsulation: ViewEncapsulation.None
})
export class MediaPlayerComponent implements OnChanges {
@@ -39,39 +38,32 @@ export class MediaPlayerComponent implements OnChanges {
mimeType: string;
@Input()
nameFile: string;
@Input()
nodeId: string;
fileName: string;
/** media subtitles for the media player*/
@Input()
tracks: Track[] = [];
@Output()
error = new EventEmitter<any>();
constructor(private contentService: ContentService, private viewUtils: ViewUtilService) {
constructor(private contentService: ContentService) {
}
ngOnChanges(changes: SimpleChanges) {
const blobFile = changes['blobFile'];
const nodeId = changes['nodeId'];
if (blobFile && blobFile.currentValue) {
this.urlFile = this.contentService.createTrustedUrl(this.blobFile);
return;
}
if (nodeId && nodeId.currentValue) {
this.viewUtils.generateMediaTracks(this.nodeId).then((tracks) => this.tracks = tracks);
}
if (!this.urlFile && !this.blobFile) {
throw new Error('Attribute urlFile or blobFile is required');
}
}
onMediaPlayerError() {
this.error.emit();
onMediaPlayerError(event: any) {
this.error.emit(event);
}
}

View File

@@ -593,8 +593,6 @@
}
}
.adf-hidden, [hidden] {
display: none !important;
}

View File

@@ -83,7 +83,7 @@
<span>{{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}</span>
</div>
<div class="adf-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
<div class="adf-pdf-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
{{ currentScaleText }}
</div>

View File

@@ -1,8 +1,3 @@
.adf-viewer-content-container {
width: 100%;
height: 100%;
}
.adf-pdf-viewer {
width: 100%;
height: 100%;
@@ -19,6 +14,35 @@
height: 100%;
width: 190px;
background-color: rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
padding: 0;
.adf-info-drawer-layout {
display: flex;
flex-direction: column;
flex: 1;
background: #e6e6e6;
}
.adf-info-drawer-layout-header {
margin-bottom: 0;
}
.adf-info-drawer-layout-content {
padding: 0;
height: 100%;
overflow: hidden;
}
.adf-info-drawer-content {
height: 100%;
}
.adf-info-drawer-layout-content > *:last-child {
height: 100%;
overflow: hidden;
}
}
.adf-thumbnails-template {
@@ -51,6 +75,17 @@
margin: auto;
max-height: 100px;
max-width: 300px;
.mat-progress-bar {
max-width: 300px;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
/* stylelint-disable-next-line property-no-vendor-prefix */
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
}
&__toolbar {
@@ -87,5 +122,17 @@
outline-color: gray;
}
}
&-page-scale {
cursor: default;
width: 79px;
height: 24px;
font-size: var(--theme-body-1-font-size);
border: 1px solid var(--theme-border-color);
text-align: center;
line-height: 24px;
margin-left: 4px;
margin-right: 4px;
}
}
}

View File

@@ -60,7 +60,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
blobFile: Blob;
@Input()
nameFile: string;
fileName: string;
@Input()
showToolbar: boolean = true;

View File

@@ -0,0 +1,102 @@
<div *ngIf="isLoading"
class="adf-viewer-render-main"
fxFlexOrder="1"
fxFlex="1 1 auto">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container">
<ng-container *ngIf="isLoading">
<div class="adf-viewer-render__loading-screen"
fxFlex="1 1 auto">
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
<div>
<mat-spinner></mat-spinner>
</div>
</div>
</ng-container>
</div>
</div>
</div>
<div *ngIf="!isLoading"
class="adf-viewer-render-main"
fxFlexOrder="1"
fxFlex="1 1 auto">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container" [ngSwitch]="viewerType">
<ng-container *ngSwitchCase="'external'">
<adf-preview-extension
*ngIf="!!externalViewer"
[id]="externalViewer.component"
[url]="urlFile"
[extension]="externalViewer.fileExtension"
[attr.data-automation-id]="externalViewer.component">
</adf-preview-extension>
</ng-container>
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer [thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFile"
[fileName]="internalFileName"
[cacheType]="cacheTypeForContent"
(close)="onClose()"
(error)="onUnsupportedFile()">
</adf-pdf-viewer>
</ng-container>
<ng-container *ngSwitchCase="'image'">
<adf-img-viewer [urlFile]="urlFile"
[readOnly]="readOnly"
[fileName]="internalFileName"
[blobFile]="blobFile"
(error)="onUnsupportedFile()"
(submit)="onSubmitFile($event)"
></adf-img-viewer>
</ng-container>
<ng-container *ngSwitchCase="'media'">
<adf-media-player id="adf-mdedia-player"
[urlFile]="urlFile"
[tracks]="tracks"
[mimeType]="mimeType"
[blobFile]="blobFile"
[fileName]="internalFileName"
(error)="onUnsupportedFile()">
</adf-media-player>
</ng-container>
<ng-container *ngSwitchCase="'text'">
<adf-txt-viewer [urlFile]="urlFile"
[blobFile]="blobFile">
</adf-txt-viewer>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<ng-container *ngFor="let ext of viewerExtensions">
<adf-preview-extension *ngIf="checkExtensions(ext.fileExtension)"
[id]="ext.component"
[url]="urlFile"
[extension]="extension"
[attr.data-automation-id]="ext.component">
</adf-preview-extension>
</ng-container>
<span class="adf-viewer-render-custom-content"
*ngFor="let extensionTemplate of extensionTemplates">
<ng-template *ngIf="extensionTemplate.isVisible"
[ngTemplateOutlet]="extensionTemplate.template"
[ngTemplateOutletContext]="{ urlFile: urlFile, extension:extension }">
</ng-template>
</span>
</ng-container>
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format></adf-viewer-unknown-format>
</ng-container>
</div>
</div>
</div>

View File

@@ -0,0 +1,69 @@
/* stylelint-disable scss/at-extend-no-missing-placeholder */
.adf-full-screen {
width: 100%;
height: 100%;
background-color: var(--theme-card-bg-color);
}
.adf-viewer-render {
&-main {
width: 0;
}
&-content-container {
display: flex;
justify-content: center;
}
&-layout-content {
@extend .adf-full-screen;
position: relative;
overflow-y: hidden;
overflow-x: hidden;
z-index: 1;
background-color: var(--theme-background-color);
display: flex;
flex-flow: row wrap;
flex: 1;
& > div {
display: flex;
flex-flow: row wrap;
margin: 0 auto;
align-items: stretch;
height: 93vh;
width: 100%;
}
}
&-overlay-container {
.adf-viewer-render-content {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
}
&__loading-screen {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.mat-spinner {
margin: 0 auto;
}
}
&-custom-content {
width: 100vw;
}
&-unknown-content {
align-items: center;
display: flex;
}
}

View File

@@ -0,0 +1,445 @@
/*!
* @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 { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RenderingQueueServices } from '../services/rendering-queue.services';
import { ViewerRenderComponent } from './viewer-render.component';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
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';
@Component({
selector: 'adf-double-viewer',
template: `
<adf-viewer-render [urlFile]="urlFileViewer1" #viewer1></adf-viewer-render>
<adf-viewer-render [urlFile]="urlFileViewer2" #viewer2></adf-viewer-render>
`
})
class DoubleViewerComponent {
@ViewChild('viewer1')
viewer1: ViewerRenderComponent;
@ViewChild('viewer2')
viewer2: ViewerRenderComponent;
urlFileViewer1: string;
urlFileViewer2: string;
}
describe('ViewerComponent', () => {
let component: ViewerRenderComponent;
let fixture: ComponentFixture<ViewerRenderComponent>;
let element: HTMLElement;
let extensionService: AppExtensionService;
setupTestBed({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
CoreTestingModule,
MatButtonModule,
MatIconModule
],
declarations: [
DoubleViewerComponent
],
providers: [
RenderingQueueServices,
{provide: Location, useClass: SpyLocation},
MatDialog
]
});
beforeEach(() => {
fixture = TestBed.createComponent(ViewerRenderComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
extensionService = TestBed.inject(AppExtensionService);
});
afterEach(() => {
fixture.destroy();
});
describe('Double viewer Test', () => {
it('should not reload the content of all the viewer after type change', async () => {
const fixtureDouble = TestBed.createComponent(DoubleViewerComponent);
await fixtureDouble.detectChanges();
await fixtureDouble.whenStable();
fixtureDouble.componentInstance.urlFileViewer1 = 'fake-test-file.pdf';
fixtureDouble.componentInstance.urlFileViewer2 = 'fake-test-file-two.xls';
fixtureDouble.componentInstance.viewer1.ngOnChanges();
fixtureDouble.componentInstance.viewer2.ngOnChanges();
await fixtureDouble.detectChanges();
await fixtureDouble.whenStable();
expect(fixtureDouble.componentInstance.viewer1.viewerType).toBe('pdf');
expect(fixtureDouble.componentInstance.viewer2.viewerType).toBe('unknown');
fixtureDouble.componentInstance.urlFileViewer1 = 'fake-test-file.pdf';
fixtureDouble.componentInstance.urlFileViewer2 = 'fake-test-file-two.png';
await fixtureDouble.detectChanges();
await fixtureDouble.whenStable();
fixtureDouble.componentInstance.viewer1.ngOnChanges();
fixtureDouble.componentInstance.viewer2.ngOnChanges();
expect(fixtureDouble.componentInstance.viewer1.viewerType).toBe('pdf');
expect(fixtureDouble.componentInstance.viewer2.viewerType).toBe('image');
});
});
describe('Extension Type Test', () => {
it('should display pdf external viewer via wildcard notation', async () => {
const extension: ViewerExtensionRef = {
component: 'custom.component',
id: 'custom.component.id',
fileExtension: '*'
};
spyOn(extensionService, 'getViewerExtensions').and.returnValue([extension]);
fixture = TestBed.createComponent(ViewerRenderComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
component.urlFile = 'fake-test-file.pdf';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(component.externalExtensions.includes('*')).toBe(true);
expect(component.externalViewer).toBe(extension);
expect(component.viewerType).toBe('external');
expect(element.querySelector('[data-automation-id="custom.component"]')).not.toBeNull();
});
it('should display pdf with the first external viewer provided', async () => {
const extensions: ViewerExtensionRef[] = [
{
component: 'custom.component.1',
id: 'custom.component.id',
fileExtension: '*'
},
{
component: 'custom.component.2',
id: 'custom.component.id',
fileExtension: '*'
}
];
spyOn(extensionService, 'getViewerExtensions').and.returnValue(extensions);
fixture = TestBed.createComponent(ViewerRenderComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
component.urlFile = 'fake-test-file.pdf';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('[data-automation-id="custom.component.1"]')).not.toBeNull();
expect(element.querySelector('[data-automation-id="custom.component.2"]')).toBeNull();
});
it('should display url with the external viewer provided', async () => {
const extension: ViewerExtensionRef = {
component: 'custom.component',
id: 'custom.component.id',
fileExtension: '*'
};
spyOn(extensionService, 'getViewerExtensions').and.returnValue([extension]);
fixture = TestBed.createComponent(ViewerRenderComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
component.urlFile = 'http://localhost:4200/alfresco';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(component.externalExtensions.includes('*')).toBe(true);
expect(component.externalViewer).toBe(extension);
expect(component.viewerType).toBe('external');
expect(element.querySelector('[data-automation-id="custom.component"]')).not.toBeNull();
});
it('should extension file pdf be loaded', (done) => {
component.urlFile = 'fake-test-file.pdf';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-pdf-viewer')).not.toBeNull();
done();
});
});
it('should extension file png be loaded', (done) => {
component.urlFile = 'fake-url-file.png';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('should extension file mp4 be loaded', (done) => {
component.urlFile = 'fake-url-file.mp4';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-media-player')).not.toBeNull();
done();
});
});
it('should extension file txt be loaded', (done) => {
component.urlFile = 'fake-test-file.txt';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-txt-viewer')).not.toBeNull();
done();
});
});
it('should display [unknown format] for unsupported extensions', (done) => {
component.urlFile = 'fake-url-file.unsupported';
component.mimeType = '';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-viewer-unknown-format')).toBeDefined();
done();
});
});
});
describe('MimeType handling', () => {
it('should display an image file identified by mimetype when the filename has no extension', (done) => {
component.urlFile = 'fake-content-img';
component.mimeType = 'image/png';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('should display a image file identified by mimetype when the file extension is wrong', (done) => {
component.urlFile = 'fake-content-img.bin';
component.mimeType = 'image/png';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('should display the txt viewer if the file identified by mimetype is a txt when the filename has wrong extension', (done) => {
component.urlFile = 'fake-content-txt.bin';
component.mimeType = 'text/plain';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-txt-viewer')).not.toBeNull();
done();
});
});
it('should display the media player if the file identified by mimetype is a media when the filename has wrong extension', (done) => {
component.urlFile = 'fake-content-video.bin';
component.mimeType = 'video/mp4';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-media-player')).not.toBeNull();
done();
});
}, 25000);
it('should display the media player if the file identified by mimetype is a media when the filename has no extension', (done) => {
component.urlFile = 'fake-content-video';
component.mimeType = 'video/mp4';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-media-player')).not.toBeNull();
done();
});
}, 25000);
it('should display a PDF file identified by mimetype when the filename has no extension', (done) => {
component.urlFile = 'fake-content-pdf';
component.mimeType = 'application/pdf';
fixture.detectChanges();
component.ngOnChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-pdf-viewer')).not.toBeNull();
done();
});
}, 25000);
it('should display a PDF file identified by mimetype when the file extension is wrong', (done) => {
component.urlFile = 'fake-content-pdf.bin';
component.mimeType = 'application/pdf';
component.ngOnChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-pdf-viewer')).not.toBeNull();
done();
});
}, 25000);
});
describe('Base component', () => {
beforeEach(() => {
component.urlFile = 'fake-test-file.pdf';
component.mimeType = 'application/pdf';
fixture.detectChanges();
});
describe('Attribute', () => {
it('should urlFile present not thrown any error ', () => {
expect(() => {
component.ngOnChanges();
}).not.toThrow();
});
});
describe('error handling', () => {
it('should switch to the unknown template if the type specific viewers throw an error', (done) => {
component.urlFile = 'fake-url-file.icns';
component.mimeType = 'image/png';
component.ngOnChanges();
fixture.detectChanges();
component.onUnsupportedFile();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-viewer-unknown-format')).toBeDefined();
done();
});
});
});
describe('Events', () => {
it('should if the extension change extension Change event be fired ', (done) => {
component.extensionChange.subscribe((fileExtension) => {
expect(fileExtension).toEqual('png');
done();
});
component.urlFile = 'fake-url-file.png';
component.ngOnChanges();
});
});
describe('display name property override by urlFile', () => {
it('should fileName override the default name if is present and urlFile is set', () => {
component.urlFile = 'fake-test-file.pdf';
component.fileName = 'test name';
fixture.detectChanges();
component.ngOnChanges();
expect(component.internalFileName).toEqual('test name');
});
it('should use the urlFile name if fileName is NOT set and urlFile is set', () => {
component.urlFile = 'fake-test-file.pdf';
component.fileName = '';
fixture.detectChanges();
component.ngOnChanges();
expect(component.internalFileName).toEqual('fake-test-file.pdf');
});
});
describe('display name property override by blobFile', () => {
it('should fileName override the name if is present and blobFile is set', () => {
component.fileName = 'blob file display name';
component.blobFile = new Blob(['This is my blob content'], {type: 'text/plain'});
fixture.detectChanges();
component.ngOnChanges();
expect(component.internalFileName).toEqual('blob file display name');
});
});
});
});

View File

@@ -0,0 +1,190 @@
/*!
* @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, EventEmitter,
Input, OnChanges, Output, TemplateRef,
ViewEncapsulation, OnInit, OnDestroy
} from '@angular/core';
import { Subject } from 'rxjs';
import { ViewUtilService } from '../services/view-util.service';
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { MatDialog } from '@angular/material/dialog';
import { Track } from '../models/viewer.model';
@Component({
selector: 'adf-viewer-render',
templateUrl: './viewer-render.component.html',
styleUrls: ['./viewer-render.component.scss'],
host: {class: 'adf-viewer-render'},
encapsulation: ViewEncapsulation.None,
providers: [ViewUtilService]
})
export class ViewerRenderComponent implements OnChanges, OnInit, OnDestroy {
/** 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 = '';
/** Loads a Blob File */
@Input()
blobFile: Blob;
/** Toggles the 'Full Screen' feature. */
@Input()
allowFullScreen = true;
/** Toggles PDF thumbnails. */
@Input()
allowThumbnails = true;
/** The template for the pdf thumbnails. */
@Input()
thumbnailsTemplate: TemplateRef<any> = null;
/** MIME type of the file content (when not determined by the filename extension). */
@Input()
mimeType: string;
/** Override Content filename. */
@Input()
fileName: string;
/** Override loading status */
@Input()
isLoading = false;
/** Enable when where is possible the editing functionalities */
@Input()
readOnly = true;
/** media subtitles for the media player*/
@Input()
tracks: Track[] = [];
/** Emitted when the filename extension changes. */
@Output()
extensionChange = new EventEmitter<string>();
/** Emitted when the img is submitted in the img viewer. */
@Output()
submitFile = new EventEmitter<Blob>();
/** Emitted when the img is submitted in the img viewer. */
@Output()
close = new EventEmitter<boolean>();
extensionTemplates: { template: TemplateRef<any>; isVisible: boolean }[] = [];
extension: string;
internalFileName: string;
viewerType: string = 'unknown';
/**
* Returns a list of the active Viewer content extensions.
*/
get viewerExtensions(): ViewerExtensionRef[] {
return this.extensionService.getViewerExtensions();
}
/**
* Provides a list of file extensions supported by external plugins.
*/
get externalExtensions(): string[] {
return this.viewerExtensions.map(ext => ext.fileExtension);
}
private _externalViewer: ViewerExtensionRef;
get externalViewer(): ViewerExtensionRef {
if (!this._externalViewer) {
this._externalViewer = this.viewerExtensions.find(ext => ext.fileExtension === '*');
}
return this._externalViewer;
}
cacheTypeForContent = '';
private onDestroy$ = new Subject<boolean>();
constructor(private viewUtilService: ViewUtilService,
private extensionService: AppExtensionService,
public dialog: MatDialog) {
}
ngOnInit() {
this.cacheTypeForContent = '';
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
ngOnChanges() {
this.isLoading = !this.blobFile && !this.urlFile;
if (this.blobFile) {
this.setUpBlobData();
} else if (this.urlFile) {
this.setUpUrlFile();
}
}
private setUpBlobData() {
this.internalFileName = this.fileName;
this.viewerType = 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);
this.viewerType = this.viewUtilService.getViewerType(this.extension, this.mimeType);
this.extensionChange.emit(this.extension);
this.scrollTop();
}
scrollTop() {
window.scrollTo(0, 1);
}
checkExtensions(extensionAllowed) {
if (typeof extensionAllowed === 'string') {
return this.extension.toLowerCase() === extensionAllowed.toLowerCase();
} else if (extensionAllowed.length > 0) {
return extensionAllowed.find((currentExtension) => this.extension.toLowerCase() === currentExtension.toLowerCase());
}
}
onSubmitFile(newImageBlob: Blob) {
this.submitFile.next(newImageBlob);
}
onUnsupportedFile() {
this.viewerType = 'unknown';
}
onClose() {
this.close.next(true);
}
}

View File

@@ -0,0 +1,32 @@
/*!
* @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 {
ChangeDetectionStrategy,
Component,
ViewEncapsulation
} from '@angular/core';
@Component({
selector: 'adf-viewer-toolbar-custom-actions',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'adf-viewer-toolbar-custom-actions' },
template: `<ng-content></ng-content>`
})
export class ViewerToolbarCustomActionsComponent{
}

View File

@@ -30,7 +30,7 @@
[attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
mat-icon-button
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}"
(click)="onBackButtonClick()">
(click)="onClose()">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar-title>
@@ -50,7 +50,7 @@
[src]="mimeType | adfMimeTypeIcon"
data-automation-id="adf-file-thumbnail">
<span class="adf-viewer__display-name"
id="adf-viewer-display-name">{{ fileTitle }}</span>
id="adf-viewer-display-name">{{ fileName }}</span>
<button *ngIf="allowNavigate && canNavigateNext"
data-automation-id="adf-toolbar-next-file"
mat-icon-button
@@ -80,29 +80,10 @@
<adf-toolbar-divider></adf-toolbar-divider>
<button id="adf-viewer-download"
*ngIf="allowDownload"
mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate }}"
data-automation-id="adf-toolbar-download"
[adfNodeDownload]="nodeEntry"
[version]="versionEntry">
<mat-icon>file_download</mat-icon>
</button>
<button id="adf-viewer-print"
*ngIf="allowPrint"
mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ACTIONS.PRINT' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.PRINT' | translate }}"
data-automation-id="adf-toolbar-print"
(click)="printContent()">
<mat-icon>print</mat-icon>
</button>
<ng-content select="adf-viewer-toolbar-custom-actions"></ng-content>
<button id="adf-viewer-fullscreen"
*ngIf="viewerType !== 'media' && allowFullScreen"
*ngIf="allowFullScreen"
mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate }}"
@@ -120,7 +101,7 @@
title="{{ 'ADF_VIEWER.ACTIONS.INFO' | translate }}"
data-automation-id="adf-toolbar-sidebar"
[color]="showRightSidebar ? 'accent' : null"
(click)="toggleSidebar()">
(click)="toggleRightSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
@@ -146,6 +127,7 @@
<div fxLayout="row"
fxFlex="1 1 auto">
<ng-container *ngIf="allowRightSidebar && showRightSidebar">
<div class="adf-viewer__sidebar"
[ngClass]="'adf-viewer__sidebar__right'"
@@ -174,115 +156,18 @@
</div>
</ng-container>
<div *ngIf="isLoading"
class="adf-viewer-main"
fxFlexOrder="1"
fxFlex="1 1 auto">
<div class="adf-viewer-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-content-container">
<ng-container *ngIf="isLoading">
<div class="adf-viewer__loading-screen"
fxFlex="1 1 auto">
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
<div>
<mat-spinner></mat-spinner>
</div>
</div>
</ng-container>
</div>
</div>
</div>
<div *ngIf="!isLoading"
class="adf-viewer-main"
fxFlexOrder="1"
fxFlex="1 1 auto">
<div class="adf-viewer-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-content-container" [ngSwitch]="viewerType">
<ng-container *ngSwitchCase="'external'">
<adf-preview-extension
*ngIf="!!externalViewer"
[id]="externalViewer.component"
[node]="nodeEntry?.entry"
[url]="urlFileContent"
[extension]="externalViewer.fileExtension"
[attr.data-automation-id]="externalViewer.component">
</adf-preview-extension>
</ng-container>
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer (close)="onBackButtonClick()"
[thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFileContent"
[nameFile]="displayName"
[cacheType]="cacheTypeForContent"
(error)="onUnsupportedFile()"></adf-pdf-viewer>
</ng-container>
<ng-container *ngSwitchCase="'image'">
<adf-img-viewer [urlFile]="urlFileContent"
[nameFile]="displayName || fileName"
[blobFile]="blobFile"
[readOnly]="readOnly"
(error)="onUnsupportedFile()"
(submit)="onSubmitFile($event)"
></adf-img-viewer>
</ng-container>
<ng-container *ngSwitchCase="'media'">
<adf-media-player id="adf-mdedia-player"
[urlFile]="urlFileContent"
[nodeId]="nodeEntry?.entry?.id"
[mimeType]="mimeType"
[blobFile]="blobFile"
[nameFile]="displayName"
(error)="onUnsupportedFile()"></adf-media-player>
</ng-container>
<ng-container *ngSwitchCase="'text'">
<adf-txt-viewer [urlFile]="urlFileContent"
[blobFile]="blobFile"></adf-txt-viewer>
</ng-container>
<ng-container *ngSwitchCase="'in_creation'">
<div class="adf-viewer__loading-screen"
fxFlex="1 1 auto">
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
<div>
<mat-spinner></mat-spinner>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<ng-container *ngFor="let ext of viewerExtensions">
<adf-preview-extension *ngIf="checkExtensions(ext.fileExtension)"
[id]="ext.component"
[node]="nodeEntry.entry"
[url]="urlFileContent"
[extension]="extension"
[attr.data-automation-id]="ext.component">
</adf-preview-extension>
</ng-container>
<span class="adf-viewer-custom-content"
*ngFor="let extensionTemplate of extensionTemplates">
<ng-template *ngIf="extensionTemplate.isVisible"
[ngTemplateOutlet]="extensionTemplate.template"
[ngTemplateOutletContext]="{ urlFileContent: urlFileContent, extension:extension }">
</ng-template>
</span>
</ng-container>
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format></adf-viewer-unknown-format>
</ng-container>
</div>
</div>
</div>
<adf-viewer-render
fxFlexOrder="1"
fxFlex="1 1 auto"
(close)="onClose()"
[mimeType]="mimeType"
[fileName]="fileName"
[blobFile]="blobFile"
[readOnly]="readOnly"
(submitFile)="onSubmitFile($event)"
[urlFile]="urlFile"
[tracks]="tracks">
</adf-viewer-render>
</div>
</div>

View File

@@ -112,32 +112,6 @@
@extend .adf-full-screen;
}
&-content-container {
display: flex;
justify-content: center;
}
&-custom-content {
width: 100vw;
}
&-unknown-content {
align-items: center;
display: flex;
}
&__loading-screen {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.mat-spinner {
margin: 0 auto;
}
}
&__sidebar {
width: 350px;
display: block;
@@ -154,52 +128,4 @@
border-right: 1px solid var(--theme-border-color);
}
}
&__thumbnails {
width: 180px;
display: flex;
flex-direction: column;
padding: 0;
background: #e6e6e6;
.adf-info-drawer-layout {
display: flex;
flex-direction: column;
flex: 1;
background: #e6e6e6;
}
.adf-info-drawer-layout-header {
margin-bottom: 0;
}
.adf-info-drawer-layout-content {
padding: 0;
height: 100%;
overflow: hidden;
}
.adf-info-drawer-content {
height: 100%;
}
.adf-info-drawer-layout-content > *:last-child {
height: 100%;
overflow: hidden;
}
}
&__toolbar {
&-page-scale {
cursor: default;
width: 79px;
height: 24px;
font-size: var(--theme-body-1-font-size);
border: 1px solid var(--theme-border-color);
text-align: center;
line-height: 24px;
margin-left: 4px;
margin-right: 4px;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,45 +16,39 @@
*/
import {
Component, ContentChild, EventEmitter, HostListener, ElementRef,
Input, OnChanges, Output, TemplateRef,
ViewEncapsulation, OnInit, OnDestroy, ChangeDetectorRef
Component,
ContentChild,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import {
SharedLinkEntry,
Node,
Version,
RenditionEntry,
NodeEntry,
VersionEntry,
SharedlinksApi, VersionsApi, NodesApi, ContentApi
} from '@alfresco/js-api';
import { BaseEvent } from '../../events';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../common/services/log.service';
import { ViewerMoreActionsComponent } from './viewer-more-actions.component';
import { ViewerOpenWithComponent } from './viewer-open-with.component';
import { ViewerSidebarComponent } from './viewer-sidebar.component';
import { ViewerToolbarComponent } from './viewer-toolbar.component';
import { fromEvent, Subject } from 'rxjs';
import { ViewUtilService } from '../services/view-util.service';
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { filter, skipWhile, takeUntil } from 'rxjs/operators';
import { fromEvent, Subject } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ContentService } from '../../services/content.service';
import { UploadService } from '../../services/upload.service';
import { FileModel } from '../../models';
import { NodesApiService } from '../../services/nodes-api.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';
import { filter, skipWhile, takeUntil } from 'rxjs/operators';
import { Track } from '../models/viewer.model';
import { ViewUtilService } from '../services/view-util.service';
@Component({
selector: 'adf-viewer',
templateUrl: './viewer.component.html',
styleUrls: ['./viewer.component.scss'],
host: { class: 'adf-viewer' },
host: {class: 'adf-viewer'},
encapsulation: ViewEncapsulation.None,
providers: [ViewUtilService]
})
export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
@ContentChild(ViewerToolbarComponent)
toolbar: ViewerToolbarComponent;
@@ -74,27 +68,29 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
@Input()
urlFile = '';
/** Viewer to use with the `urlFile` address (`pdf`, `image`, `media`, `text`).
* Used when `urlFile` has no filename and extension.
*/
@Input()
urlFileViewer: string = null;
/** Loads a Blob File */
@Input()
blobFile: Blob;
/** Node Id of the file to load. */
/** Override Content filename. */
@Input()
nodeId: string = null;
fileName: string;
/** Version Id of the file to load. */
/** Hide or show the viewer */
@Input()
versionId: string = null;
showViewer = true;
/** Shared link id (to display shared file). */
/** Allows `back` navigation */
@Input()
sharedLinkId: string = null;
allowGoBack = true;
/** Toggles the 'Full Screen' feature. */
@Input()
allowFullScreen = 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.
@@ -102,35 +98,6 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
@Input()
overlayMode = false;
/** Hide or show the viewer */
@Input()
showViewer = true;
/** Hide or show the toolbar */
@Input()
showToolbar = true;
/** Specifies the name of the file when it is not available from the URL. */
@Input()
displayName: string;
/** @deprecated 3.2.0 */
/** Allows `back` navigation */
@Input()
allowGoBack = true;
/** Toggles downloading. */
@Input()
allowDownload = true;
/** Toggles printing. */
@Input()
allowPrint = false;
/** Toggles the 'Full Screen' feature. */
@Input()
allowFullScreen = true;
/** Toggles before/next navigation. You can use the arrow buttons to navigate
* between documents in the collection.
*/
@@ -153,10 +120,6 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
@Input()
allowRightSidebar = false;
/** Toggles PDF thumbnails. */
@Input()
allowThumbnails = true;
/** Toggles right sidebar visibility. Requires `allowRightSidebar` to be set to `true`. */
@Input()
showRightSidebar = false;
@@ -173,39 +136,28 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
@Input()
sidebarLeftTemplate: TemplateRef<any> = null;
/** The template for the pdf thumbnails. */
/** Enable when where is possible the editing functionalities */
@Input()
thumbnailsTemplate: TemplateRef<any> = null;
readOnly = true;
/** media subtitles for the media player*/
@Input()
tracks: Track[] = [];
/** MIME type of the file content (when not determined by the filename extension). */
@Input()
mimeType: string;
/** Content filename. */
@Input()
fileName: string;
/** Number of times the Viewer will retry fetching content Rendition.
* There is a delay of at least one second between attempts.
/**
* Context object available for binding by the local sidebarRightTemplate with let declarations.
*/
@Input()
maxRetries = 30;
sidebarRightTemplateContext: T = null;
/** Emitted when user clicks the 'Back' button. */
@Output()
goBack = new EventEmitter<BaseEvent<any>>();
/** Emitted when user clicks the 'Print' button. */
@Output()
print = new EventEmitter<BaseEvent<any>>();
/** Emitted when the viewer is shown or hidden. */
@Output()
showViewerChange = new EventEmitter<boolean>();
/** Emitted when the filename extension changes. */
@Output()
extensionChange = new EventEmitter<string>();
/**
* Context object available for binding by the local sidebarLeftTemplate with let declarations.
*/
@Input()
sidebarLeftTemplateContext: T = null;
/** Emitted when user clicks 'Navigate Before' ("<") button. */
@Output()
@@ -215,377 +167,64 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
@Output()
navigateNext = new EventEmitter<MouseEvent | KeyboardEvent>();
/** Emitted when the shared link used is not valid. */
/** Emitted when the viewer close */
@Output()
invalidSharedLink = new EventEmitter();
showViewerChange = new EventEmitter<boolean>();
viewerType = 'unknown';
isLoading = false;
nodeEntry: NodeEntry;
versionEntry: VersionEntry;
extensionTemplates: { template: TemplateRef<any>; isVisible: boolean }[] = [];
urlFileContent: string;
otherMenu: any;
extension: string;
sidebarRightTemplateContext: { node: Node } = { node: null };
sidebarLeftTemplateContext: { node: Node } = { node: null };
fileTitle: string;
/**
* Returns a list of the active Viewer content extensions.
*/
get viewerExtensions(): ViewerExtensionRef[] {
return this.extensionService.getViewerExtensions();
}
/**
* Provides a list of file extensions supported by external plugins.
*/
get externalExtensions(): string[] {
return this.viewerExtensions.map(ext => ext.fileExtension);
}
private _externalViewer: ViewerExtensionRef;
get externalViewer(): ViewerExtensionRef {
if (!this._externalViewer) {
this._externalViewer = this.viewerExtensions.find(ext => ext.fileExtension === '*');
}
return this._externalViewer;
}
readOnly = true;
private cacheBusterNumber: number;
cacheTypeForContent = '';
// Extensions that are supported by the Viewer without conversion
private extensions = {
image: ['png', 'jpg', 'jpeg', 'gif', 'bpm', 'svg'],
media: ['wav', 'mp4', 'mp3', 'webm', 'ogg'],
text: ['txt', 'xml', 'html', 'json', 'ts', 'css', 'md'],
pdf: ['pdf']
};
// Mime types that are supported by the Viewer without conversion
private mimeTypes = {
text: ['text/plain', 'text/csv', 'text/xml', 'text/html', 'application/x-javascript'],
pdf: ['application/pdf'],
image: ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/svg+xml'],
media: ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav']
};
/** Emitted when the img is submitted in the img viewer. */
@Output()
submitFile = new EventEmitter<Blob>();
private onDestroy$ = new Subject<boolean>();
private shouldCloseViewer = true;
private closeViewer = true;
private keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown');
_sharedLinksApi: SharedlinksApi;
get sharedLinksApi(): SharedlinksApi {
this._sharedLinksApi = this._sharedLinksApi ?? new SharedlinksApi(this.apiService.getInstance());
return this._sharedLinksApi;
}
_versionsApi: VersionsApi;
get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.apiService.getInstance());
return this._versionsApi;
}
_nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance());
return this._nodesApi;
}
_contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
constructor(private apiService: AlfrescoApiService,
private nodesApiService: NodesApiService,
private viewUtilService: ViewUtilService,
private logService: LogService,
private extensionService: AppExtensionService,
private contentService: ContentService,
private uploadService: UploadService,
private el: ElementRef,
constructor(private el: ElementRef,
public dialog: MatDialog,
private cdr: ChangeDetectorRef) {
viewUtilService.maxRetries = this.maxRetries;
private viewUtilsService: ViewUtilService
) {
}
isSourceDefined(): boolean {
return !!(this.urlFile || this.blobFile || this.nodeId || this.sharedLinkId);
ngOnChanges(changes: SimpleChanges){
const { blobFile, urlFile } = changes;
if(blobFile?.currentValue){
this.mimeType = blobFile.currentValue.type;
}
if(urlFile?.currentValue){
this.fileName = this.fileName ? this.fileName : this.viewUtilsService.getFilenameFromUrl(urlFile.currentValue);
}
}
ngOnInit() {
this.nodesApiService.nodeUpdated.pipe(
filter((node) => node && node.id === this.nodeId &&
(node.name !== this.fileName ||
this.getNodeVersionProperty(this.nodeEntry.entry) !== this.getNodeVersionProperty(node))),
takeUntil(this.onDestroy$)
).subscribe((node) => this.onNodeUpdated(node));
this.viewUtilService.viewerTypeChange.pipe(takeUntil(this.onDestroy$)).subscribe((type: string) => {
this.viewerType = type;
});
this.viewUtilService.urlFileContentChange.pipe(takeUntil(this.onDestroy$)).subscribe((content: string) => {
this.urlFileContent = content;
});
ngOnInit(): void {
this.closeOverlayManager();
this.cacheTypeForContent = '';
}
private getNodeVersionProperty(node: Node): string {
return node?.properties['cm:versionLabel'] ?? '';
}
private closeOverlayManager() {
this.dialog.afterOpened.pipe(
skipWhile(() => !this.overlayMode),
takeUntil(this.onDestroy$)
).subscribe(() => this.closeViewer = false);
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
this.dialog.afterAllClosed.pipe(
skipWhile(() => !this.overlayMode),
takeUntil(this.onDestroy$)
).subscribe(() => this.closeViewer = true);
private onNodeUpdated(node: Node) {
if (node && node.id === this.nodeId) {
this.cacheTypeForContent = 'no-cache';
this.generateCacheBusterNumber();
this.isLoading = true;
this.setUpNodeFile(node).then(() => {
this.isLoading = false;
});
}
}
this.keyDown$.pipe(
skipWhile(() => !this.overlayMode),
filter((e: KeyboardEvent) => e.keyCode === 27),
takeUntil(this.onDestroy$)
).subscribe((event: KeyboardEvent) => {
event.preventDefault();
ngOnChanges() {
if (this.showViewer) {
if (!this.isSourceDefined()) {
throw new Error('A content source attribute value is missing.');
if (this.closeViewer) {
this.onClose();
}
this.isLoading = true;
if (this.blobFile) {
this.setUpBlobData();
this.isLoading = false;
} else if (this.urlFile) {
this.setUpUrlFile();
this.isLoading = false;
} else if (this.nodeId) {
this.setupNode();
} else if (this.sharedLinkId) {
this.setupSharedLink();
}
}
}
private setupSharedLink() {
this.allowGoBack = false;
this.sharedLinksApi.getSharedLink(this.sharedLinkId).then(
(sharedLinkEntry: SharedLinkEntry) => {
this.setUpSharedLinkFile(sharedLinkEntry);
this.isLoading = false;
},
() => {
this.isLoading = false;
this.logService.error('This sharedLink does not exist');
this.invalidSharedLink.next();
});
}
private setupNode() {
this.nodesApi.getNode(this.nodeId, { include: ['allowableOperations'] }).then(
(node: NodeEntry) => {
this.nodeEntry = node;
if (this.versionId) {
this.versionsApi.getVersion(this.nodeId, this.versionId).then(
(version: VersionEntry) => {
this.versionEntry = version;
this.setUpNodeFile(node.entry, version.entry).then(() => {
this.isLoading = false;
});
}
);
} else {
this.setUpNodeFile(node.entry).then(() => {
this.isLoading = false;
this.cdr.detectChanges();
});
}
},
() => {
this.isLoading = false;
this.logService.error('This node does not exist');
}
);
}
private setUpBlobData() {
this.fileTitle = this.getDisplayName('Unknown');
this.mimeType = this.blobFile.type;
this.viewerType = this.getViewerTypeByMimeType(this.mimeType);
this.allowDownload = false;
// TODO: wrap blob into the data url and allow downloading
this.extensionChange.emit(this.mimeType);
this.scrollTop();
}
private setUpUrlFile() {
const filenameFromUrl = this.getFilenameFromUrl(this.urlFile);
this.fileTitle = this.getDisplayName(filenameFromUrl);
this.extension = this.getFileExtension(filenameFromUrl);
this.urlFileContent = this.urlFile;
this.fileName = this.displayName;
this.viewerType = this.urlFileViewer || this.getViewerType(this.extension, this.mimeType);
this.extensionChange.emit(this.extension);
this.scrollTop();
}
private async setUpNodeFile(nodeData: Node, versionData?: Version): Promise<void> {
this.readOnly = !this.contentService.hasAllowableOperations(nodeData, 'update');
if (versionData && versionData.content) {
this.mimeType = versionData.content.mimeType;
} else if (nodeData.content) {
this.mimeType = nodeData.content.mimeType;
}
this.fileTitle = this.getDisplayName(versionData ? versionData.name : nodeData.name);
const currentFileVersion = this.nodeEntry?.entry?.properties && this.nodeEntry.entry.properties['cm:versionLabel'] ?
encodeURI(this.nodeEntry?.entry?.properties['cm:versionLabel']) : encodeURI('1.0');
this.urlFileContent = versionData ? this.contentApi.getVersionContentUrl(this.nodeId, versionData.id) :
this.contentApi.getContentUrl(this.nodeId);
this.urlFileContent = this.cacheBusterNumber ? this.urlFileContent + '&' + currentFileVersion + '&' + this.cacheBusterNumber :
this.urlFileContent + '&' + currentFileVersion;
this.extension = this.getFileExtension(versionData ? versionData.name : nodeData.name);
this.fileName = versionData ? versionData.name : nodeData.name;
this.viewerType = this.getViewerType(this.extension, this.mimeType);
if (this.viewerType === 'unknown') {
if (versionData) {
await this.viewUtilService.displayNodeRendition(nodeData.id, versionData.id);
} else {
await this.viewUtilService.displayNodeRendition(nodeData.id);
}
}
this.extensionChange.emit(this.extension);
this.sidebarRightTemplateContext.node = nodeData;
this.sidebarLeftTemplateContext.node = nodeData;
this.scrollTop();
}
private getViewerType(extension: string, mimeType: string): string {
let viewerType = this.getViewerTypeByExtension(extension);
if (viewerType === 'unknown') {
viewerType = this.getViewerTypeByMimeType(mimeType);
}
return viewerType;
}
private setUpSharedLinkFile(details: any) {
this.mimeType = details.entry.content.mimeType;
this.fileTitle = this.getDisplayName(details.entry.name);
this.extension = this.getFileExtension(details.entry.name);
this.fileName = details.entry.name;
this.urlFileContent = this.contentApi.getSharedLinkContentUrl(this.sharedLinkId, false);
this.viewerType = this.getViewerType(this.extension, this.mimeType);
if (this.viewerType === 'unknown') {
this.displaySharedLinkRendition(this.sharedLinkId);
}
this.extensionChange.emit(this.extension);
}
toggleSidebar() {
this.showRightSidebar = !this.showRightSidebar;
if (this.showRightSidebar && this.nodeId) {
this.nodesApi.getNode(this.nodeId, { include: ['allowableOperations'] })
.then((nodeEntry: NodeEntry) => {
this.sidebarRightTemplateContext.node = nodeEntry.entry;
});
}
}
toggleLeftSidebar() {
this.showLeftSidebar = !this.showLeftSidebar;
if (this.showRightSidebar && this.nodeId) {
this.nodesApi.getNode(this.nodeId, { include: ['allowableOperations'] })
.then((nodeEntry: NodeEntry) => {
this.sidebarLeftTemplateContext.node = nodeEntry.entry;
});
}
}
private getDisplayName(name) {
return this.displayName || name;
}
scrollTop() {
window.scrollTo(0, 1);
}
getViewerTypeByMimeType(mimeType: string) {
if (mimeType) {
mimeType = mimeType.toLowerCase();
const editorTypes = Object.keys(this.mimeTypes);
for (const type of editorTypes) {
if (this.mimeTypes[type].indexOf(mimeType) >= 0) {
return type;
}
}
}
return 'unknown';
}
getViewerTypeByExtension(extension: string): string {
if (extension) {
extension = extension.toLowerCase();
}
if (this.isExternalViewer()) {
return 'external';
}
if (this.isCustomViewerExtension(extension)) {
return 'custom';
}
if (this.extensions.image.indexOf(extension) >= 0) {
return 'image';
}
if (this.extensions.media.indexOf(extension) >= 0) {
return 'media';
}
if (this.extensions.text.indexOf(extension) >= 0) {
return 'text';
}
if (this.extensions.pdf.indexOf(extension) >= 0) {
return 'pdf';
}
return 'unknown';
}
onBackButtonClick() {
this.close();
});
}
onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) {
@@ -599,64 +238,19 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
/**
* close the viewer
*/
close() {
if (this.otherMenu) {
this.otherMenu.hidden = false;
}
onClose() {
this.showViewer = false;
this.showViewerChange.emit(this.showViewer);
}
/**
* get File name from url
*
* @param url - url file
*/
getFilenameFromUrl(url: string): string {
const anchor = url.indexOf('#');
const query = url.indexOf('?');
const end = Math.min(
anchor > 0 ? anchor : url.length,
query > 0 ? query : url.length);
return url.substring(url.lastIndexOf('/', end) + 1, end);
toggleRightSidebar() {
this.showRightSidebar = !this.showRightSidebar;
}
/**
* Get file extension from the string.
* Supports the URL formats like:
* http://localhost/test.jpg?cache=1000
* http://localhost/test.jpg#cache=1000
*
* @param fileName - file name
*/
getFileExtension(fileName: string): string {
if (fileName) {
const match = fileName.match(/\.([^\./\?\#]+)($|\?|\#)/);
return match ? match[1] : null;
}
return null;
toggleLeftSidebar() {
this.showLeftSidebar = !this.showLeftSidebar;
}
private isExternalViewer(): boolean {
return !!this.viewerExtensions.find(ext => ext.fileExtension === '*');
}
isCustomViewerExtension(extension: string): boolean {
const extensions = this.externalExtensions || [];
if (extension && extensions.length > 0) {
extension = extension.toLowerCase();
return extensions.flat().indexOf(extension) >= 0;
}
return false;
}
/**
* Keyboard event listener
*
* @param event
*/
@HostListener('document:keyup', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
if (event && event.defaultPrevented) {
@@ -684,17 +278,6 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
}
}
printContent() {
if (this.allowPrint) {
const args = new BaseEvent();
this.print.next(args);
if (!args.defaultPrevented) {
this.viewUtilService.printFileGeneric(this.nodeId, this.mimeType);
}
}
}
/**
* Triggers full screen mode with a main content area displayed.
*/
@@ -715,82 +298,13 @@ export class ViewerComponent implements OnChanges, OnInit, OnDestroy {
}
}
private async displaySharedLinkRendition(sharedId: string) {
try {
const rendition: RenditionEntry = await this.sharedLinksApi.getSharedLinkRendition(sharedId, 'pdf');
if (rendition.entry.status.toString() === 'CREATED') {
this.viewerType = 'pdf';
this.urlFileContent = this.contentApi.getSharedLinkRenditionUrl(sharedId, 'pdf');
}
} catch (error) {
this.logService.error(error);
try {
const rendition: RenditionEntry = await this.sharedLinksApi.getSharedLinkRendition(sharedId, 'imgpreview');
if (rendition.entry.status.toString() === 'CREATED') {
this.viewerType = 'image';
this.urlFileContent = this.contentApi.getSharedLinkRenditionUrl(sharedId, 'imgpreview');
}
} catch (renditionError) {
this.logService.error(renditionError);
}
}
}
checkExtensions(extensionAllowed) {
if (typeof extensionAllowed === 'string') {
return this.extension.toLowerCase() === extensionAllowed.toLowerCase();
} else if (extensionAllowed.length > 0) {
return extensionAllowed.find((currentExtension) => this.extension.toLowerCase() === currentExtension.toLowerCase());
}
}
onSubmitFile(newImageBlob: Blob) {
if (this?.nodeEntry?.entry?.id && !this.readOnly) {
const newImageFile: File = new File([newImageBlob], this?.nodeEntry?.entry?.name, { type: this?.nodeEntry?.entry?.content?.mimeType });
const newFile = new FileModel(
newImageFile,
{
majorVersion: false,
newVersion: true,
parentId: this?.nodeEntry?.entry?.parentId,
nodeType: this?.nodeEntry?.entry?.content?.mimeType
},
this?.nodeEntry?.entry?.id
);
this.uploadService.addToQueue(...[newFile]);
this.uploadService.uploadFilesInTheQueue();
}
this.submitFile.emit(newImageBlob);
}
onUnsupportedFile() {
this.viewerType = 'unknown';
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private closeOverlayManager() {
this.dialog.afterOpened.pipe(
skipWhile(() => !this.overlayMode),
takeUntil(this.onDestroy$)
).subscribe(() => this.shouldCloseViewer = false);
this.dialog.afterAllClosed.pipe(
skipWhile(() => !this.overlayMode),
takeUntil(this.onDestroy$)
).subscribe(() => this.shouldCloseViewer = true);
this.keyDown$.pipe(
skipWhile(() => !this.overlayMode),
filter((e: KeyboardEvent) => e.keyCode === 27),
takeUntil(this.onDestroy$)
).subscribe((event: KeyboardEvent) => {
event.preventDefault();
if (this.shouldCloseViewer) {
this.close();
}
});
}
private generateCacheBusterNumber() {
this.cacheBusterNumber = Date.now();
}
}

View File

@@ -19,7 +19,7 @@ import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing';
import { ChangeDetectorRef, ElementRef } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ViewerComponent } from '../components/viewer.component';
import { ViewerRenderComponent } from '../components/viewer-render.component';
import { ViewerExtensionDirective } from './viewer-extension.directive';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
@@ -43,7 +43,7 @@ describe('ExtensionViewerDirective', () => {
{ provide: Location, useClass: SpyLocation },
ViewerExtensionDirective,
{provide: ElementRef, useClass: MockElementRef},
ViewerComponent,
ViewerRenderComponent,
{ provide: ChangeDetectorRef, useValue: { detectChanges: () => {} } }
]
});

View File

@@ -16,7 +16,7 @@
*/
import { AfterContentInit, ContentChild, Directive, Input, TemplateRef, OnDestroy } from '@angular/core';
import { ViewerComponent } from '../components/viewer.component';
import { ViewerRenderComponent } from '../components/viewer-render.component';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -41,7 +41,7 @@ export class ViewerExtensionDirective implements AfterContentInit, OnDestroy {
private onDestroy$ = new Subject<boolean>();
constructor(private viewerComponent: ViewerComponent) {
constructor(private viewerComponent: ViewerRenderComponent) {
}
ngAfterContentInit() {

View File

@@ -16,7 +16,6 @@
*/
export * from './services/view-util.service';
export * from './components/viewer.component';
export * from './components/img-viewer.component';
export * from './components/media-player.component';
export * from './components/pdf-viewer-password-dialog';
@@ -31,6 +30,8 @@ export * from './components/viewer-open-with.component';
export * from './components/viewer-sidebar.component';
export * from './components/viewer-toolbar.component';
export * from './components/viewer-toolbar-actions.component';
export * from './components/viewer-toolbar-custom-actions.component';
export * from './components/viewer-render.component';
export * from './components/viewer.component';
export * from './directives/viewer-extension.directive';

View File

@@ -16,158 +16,87 @@
*/
import { Injectable } from '@angular/core';
import { ContentApi, RenditionEntry, RenditionPaging, RenditionsApi, VersionsApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../common/services/log.service';
import { Subject } from 'rxjs';
import { Track } from '../models/viewer.model';
import { TranslationService } from '../../translation/translation.service';
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
@Injectable({
providedIn: 'root'
})
export class ViewUtilService {
static TARGET = '_new';
/**
* Content groups based on categorization of files that can be viewed in the web browser. This
* implementation or grouping is tied to the definition the ng component: ViewerComponent
*/
static ContentGroup = {
IMAGE: 'image',
MEDIA: 'media',
PDF: 'pdf',
TEXT: 'text'
// Extensions that are supported by the Viewer without conversion
private extensions = {
image: ['png', 'jpg', 'jpeg', 'gif', 'bpm', 'svg'],
media: ['wav', 'mp4', 'mp3', 'webm', 'ogg'],
text: ['txt', 'xml', 'html', 'json', 'ts', 'css', 'md'],
pdf: ['pdf']
};
/**
* The name of the rendition with the media subtitles in the supported format
*/
static SUBTITLES_RENDITION_NAME = 'webvtt';
/**
* Based on ViewerComponent Implementation, this value is used to determine how many times we try
* to get the rendition of a file for preview, or printing.
*/
maxRetries = 5;
/**
* Mime-type grouping based on the ViewerComponent.
*/
// Mime types that are supported by the Viewer without conversion
private mimeTypes = {
text: ['text/plain', 'text/csv', 'text/xml', 'text/html', 'application/x-javascript'],
pdf: ['application/pdf'],
image: ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/svg+xml'],
media: ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/ogg', 'audio/wav']
media: ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav']
};
/**
* Timeout used for setInterval.
* Returns a list of the active Viewer content extensions.
*/
TRY_TIMEOUT: number = 10000;
/**
* Subscribers needed for ViewerComponent to update the viewerType and urlFileContent.
*/
viewerTypeChange: Subject<string> = new Subject<string>();
urlFileContentChange: Subject<string> = new Subject<string>();
private _renditionsApi: RenditionsApi;
get renditionsApi(): RenditionsApi {
this._renditionsApi = this._renditionsApi ?? new RenditionsApi(this.apiService.getInstance());
return this._renditionsApi;
}
private _contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
private _versionsApi: VersionsApi;
get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.apiService.getInstance());
return this._versionsApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService,
private translateService: TranslationService) {
get viewerExtensions(): ViewerExtensionRef[] {
return this.extensionService.getViewerExtensions();
}
/**
* This method takes a url to trigger the print dialog against, and the type of artifact that it
* is.
* This URL should be one that can be rendered in the browser, for example PDF, Image, or Text
* Provides a list of file extensions supported by external plugins.
*/
printFile(url: string, type: string): void {
const pwa = window.open(url, ViewUtilService.TARGET);
if (pwa) {
// Because of the way chrome focus and close image window vs. pdf preview window
if (type === ViewUtilService.ContentGroup.IMAGE) {
pwa.onfocus = () => {
setTimeout(() => {
pwa.close();
}, 500);
};
}
get externalExtensions(): string[] {
return this.viewerExtensions.map(ext => ext.fileExtension);
}
pwa.onload = () => {
pwa.print();
};
constructor(private extensionService: AppExtensionService) {
}
/**
* get File name from url
*
* @param url - url file
*/
getFilenameFromUrl(url: string): string {
const anchor = url.indexOf('#');
const query = url.indexOf('?');
const end = Math.min(
anchor > 0 ? anchor : url.length,
query > 0 ? query : url.length);
return url.substring(url.lastIndexOf('/', end) + 1, end);
}
/**
* Get file extension from the string.
* Supports the URL formats like:
* http://localhost/test.jpg?cache=1000
* http://localhost/test.jpg#cache=1000
*
* @param fileName - file name
*/
getFileExtension(fileName: string): string {
if (fileName) {
const match = fileName.match(/\.([^\./\?\#]+)($|\?|\#)/);
return match ? match[1] : null;
}
return null;
}
/**
* Launch the File Print dialog from anywhere other than the preview service, which resolves the
* rendition of the object that can be printed from a web browser.
* These are: images, PDF files, or PDF rendition of files.
* We also force PDF rendition for TEXT type objects, otherwise the default URL is to download.
* TODO there are different TEXT type objects, (HTML, plaintext, xml, etc. we should determine how these are handled)
*/
printFileGeneric(objectId: string, mimeType: string): void {
const nodeId = objectId;
const type: string = this.getViewerTypeByMimeType(mimeType);
getViewerType(extension: string, mimeType: string): string {
let viewerType = this.getViewerTypeByExtension(extension);
this.getRendition(nodeId, ViewUtilService.ContentGroup.PDF)
.then((value) => {
const url: string = this.getRenditionUrl(nodeId, type, (!!value));
const printType = (type === ViewUtilService.ContentGroup.PDF
|| type === ViewUtilService.ContentGroup.TEXT)
? ViewUtilService.ContentGroup.PDF : type;
this.printFile(url, printType);
})
.catch((err) => {
this.logService.error('Error with Printing');
this.logService.error(err);
});
}
getRenditionUrl(nodeId: string, type: string, renditionExists: boolean): string {
return (renditionExists && type !== ViewUtilService.ContentGroup.IMAGE) ?
this.contentApi.getRenditionUrl(nodeId, ViewUtilService.ContentGroup.PDF) :
this.contentApi.getContentUrl(nodeId, false);
}
private async waitRendition(nodeId: string, renditionId: string, retries: number): Promise<RenditionEntry> {
const rendition = await this.renditionsApi.getRendition(nodeId, renditionId);
if (this.maxRetries < retries) {
const status = rendition.entry.status.toString();
if (status === 'CREATED') {
return rendition;
} else {
retries += 1;
await this.wait(1000);
return this.waitRendition(nodeId, renditionId, retries);
}
if (viewerType === 'unknown') {
viewerType = this.getViewerTypeByMimeType(mimeType);
}
return Promise.resolve(null);
return viewerType;
}
getViewerTypeByMimeType(mimeType: string): string {
getViewerTypeByMimeType(mimeType: string) {
if (mimeType) {
mimeType = mimeType.toLowerCase();
@@ -181,164 +110,51 @@ export class ViewUtilService {
return 'unknown';
}
wait(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async getRendition(nodeId: string, renditionId: string): Promise<RenditionEntry> {
const renditionPaging: RenditionPaging = await this.renditionsApi.listRenditions(nodeId);
let rendition: RenditionEntry = renditionPaging.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
if (rendition) {
const status = rendition.entry.status.toString();
if (status === 'NOT_CREATED') {
try {
await this.renditionsApi.createRendition(nodeId, { id: renditionId });
rendition = await this.waitRendition(nodeId, renditionId, 0);
} catch (err) {
this.logService.error(err);
}
}
}
return new Promise<RenditionEntry>((resolve) => resolve(rendition));
}
async displayNodeRendition(nodeId: string, versionId?: string) {
try {
const rendition = versionId ? await this.resolveNodeRendition(nodeId, 'pdf', versionId) :
await this.resolveNodeRendition(nodeId, 'pdf');
if (rendition) {
const renditionId = rendition.entry.id;
if (renditionId === 'pdf') {
this.viewerTypeChange.next('pdf');
} else if (renditionId === 'imgpreview') {
this.viewerTypeChange.next('image');
}
const urlFileContent = versionId ? this.contentApi.getVersionRenditionUrl(nodeId, versionId, renditionId) :
this.contentApi.getRenditionUrl(nodeId, renditionId);
this.urlFileContentChange.next(urlFileContent);
}
} catch (err) {
this.logService.error(err);
}
}
private async resolveNodeRendition(nodeId: string, renditionId: string, versionId?: string): Promise<RenditionEntry> {
renditionId = renditionId.toLowerCase();
const supportedRendition: RenditionPaging = versionId ? await this.versionsApi.listVersionRenditions(nodeId, versionId) :
await this.renditionsApi.listRenditions(nodeId);
let rendition: RenditionEntry = supportedRendition.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
if (!rendition) {
renditionId = 'imgpreview';
rendition = supportedRendition.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
private getViewerTypeByExtension(extension: string): string {
if (extension) {
extension = extension.toLowerCase();
}
if (rendition) {
const status: string = rendition.entry.status.toString();
if (status === 'NOT_CREATED') {
try {
if (versionId) {
await this.versionsApi.createVersionRendition(nodeId, versionId, { id: renditionId }).then(() => {
this.viewerTypeChange.next('in_creation');
});
} else {
await this.renditionsApi.createRendition(nodeId, { id: renditionId }).then(() => {
this.viewerTypeChange.next('in_creation');
});
}
try {
rendition = versionId ? await this.waitNodeRendition(nodeId, renditionId, versionId) : await this.waitNodeRendition(nodeId, renditionId);
} catch (e) {
this.viewerTypeChange.next('error_in_creation');
rendition = null;
}
} catch (err) {
this.logService.error(err);
}
}
if (this.isExternalViewer()) {
return 'external';
}
return rendition;
}
private async waitNodeRendition(nodeId: string, renditionId: string, versionId?: string): Promise<RenditionEntry> {
let currentRetry: number = 0;
return new Promise<RenditionEntry>((resolve, reject) => {
const intervalId = setInterval(() => {
currentRetry++;
if (this.maxRetries >= currentRetry) {
if (versionId) {
this.versionsApi.getVersionRendition(nodeId, versionId, renditionId).then((rendition: RenditionEntry) => {
const status: string = rendition.entry.status.toString();
if (status === 'CREATED') {
this.handleNodeRendition(nodeId, renditionId, versionId);
clearInterval(intervalId);
return resolve(rendition);
}
}, () => reject());
} else {
this.renditionsApi.getRendition(nodeId, renditionId).then((rendition: RenditionEntry) => {
const status: string = rendition.entry.status.toString();
if (status === 'CREATED') {
this.handleNodeRendition(nodeId, renditionId);
clearInterval(intervalId);
return resolve(rendition);
}
}, () => reject());
}
} else {
clearInterval(intervalId);
return reject();
}
}, this.TRY_TIMEOUT);
});
}
private async handleNodeRendition(nodeId: string, renditionId: string, versionId?: string) {
if (renditionId === 'pdf') {
this.viewerTypeChange.next('pdf');
} else if (renditionId === 'imgpreview') {
this.viewerTypeChange.next('image');
if (this.isCustomViewerExtension(extension)) {
return 'custom';
}
const urlFileContent = versionId ? this.contentApi.getVersionRenditionUrl(nodeId, versionId, renditionId) :
this.contentApi.getRenditionUrl(nodeId, renditionId);
this.urlFileContentChange.next(urlFileContent);
if (this.extensions.image.indexOf(extension) >= 0) {
return 'image';
}
if (this.extensions.media.indexOf(extension) >= 0) {
return 'media';
}
if (this.extensions.text.indexOf(extension) >= 0) {
return 'text';
}
if (this.extensions.pdf.indexOf(extension) >= 0) {
return 'pdf';
}
return 'unknown';
}
async generateMediaTracks(nodeId: string): Promise<Track[]> {
return this.isRenditionAvailable(nodeId, ViewUtilService.SUBTITLES_RENDITION_NAME)
.then((value) => {
const tracks = [];
if (value) {
tracks.push({
kind: 'subtitles',
src: this.contentApi.getRenditionUrl(nodeId, ViewUtilService.SUBTITLES_RENDITION_NAME),
label: this.translateService.instant('ADF_VIEWER.SUBTITLES')
});
}
return tracks;
})
.catch((err) => {
this.logService.error('Error while retrieving ' + ViewUtilService.SUBTITLES_RENDITION_NAME + ' rendition');
this.logService.error(err);
return [];
});
private isExternalViewer(): boolean {
return !!this.viewerExtensions.find(ext => ext.fileExtension === '*');
}
private async isRenditionAvailable(nodeId: string, renditionId: string): Promise<boolean> {
const renditionPaging: RenditionPaging = await this.renditionsApi.listRenditions(nodeId);
const rendition: RenditionEntry = renditionPaging.list.entries.find((renditionEntry: RenditionEntry) => renditionEntry.entry.id.toLowerCase() === renditionId);
isCustomViewerExtension(extension: string): boolean {
const extensions = this.externalExtensions || [];
return rendition?.entry?.status?.toString() === 'CREATED' || false;
if (extension && extensions.length > 0) {
extension = extension.toLowerCase();
return extensions.flat().indexOf(extension) >= 0;
}
return false;
}
}

View File

@@ -38,11 +38,13 @@ import { ViewerMoreActionsComponent } from './components/viewer-more-actions.com
import { ViewerOpenWithComponent } from './components/viewer-open-with.component';
import { ViewerSidebarComponent } from './components/viewer-sidebar.component';
import { ViewerToolbarComponent } from './components/viewer-toolbar.component';
import { ViewerComponent } from './components/viewer.component';
import { ViewerRenderComponent } from './components/viewer-render.component';
import { ViewerExtensionDirective } from './directives/viewer-extension.directive';
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';
import { ViewerToolbarCustomActionsComponent } from './components/viewer-toolbar-custom-actions.component';
@NgModule({
imports: [
@@ -60,7 +62,7 @@ import { A11yModule } from '@angular/cdk/a11y';
],
declarations: [
PdfPasswordDialogComponent,
ViewerComponent,
ViewerRenderComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
@@ -73,10 +75,12 @@ import { A11yModule } from '@angular/cdk/a11y';
ViewerSidebarComponent,
ViewerOpenWithComponent,
ViewerMoreActionsComponent,
ViewerToolbarActionsComponent
ViewerToolbarActionsComponent,
ViewerComponent,
ViewerToolbarCustomActionsComponent
],
exports: [
ViewerComponent,
ViewerRenderComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
@@ -90,7 +94,9 @@ import { A11yModule } from '@angular/cdk/a11y';
ViewerSidebarComponent,
ViewerOpenWithComponent,
ViewerMoreActionsComponent,
ViewerToolbarActionsComponent
ViewerToolbarActionsComponent,
ViewerComponent,
ViewerToolbarCustomActionsComponent
]
})
export class ViewerModule {

View File

@@ -27,7 +27,6 @@ import {
OnChanges
} from '@angular/core';
import { ExtensionService } from '../../services/extension.service';
import { Node } from '@alfresco/js-api';
@Component({
selector: 'adf-preview-extension',
@@ -51,10 +50,6 @@ export class PreviewExtensionComponent implements OnInit, OnChanges, OnDestroy {
@Input()
extension: string;
/** Node containing the content to display. */
@Input()
node: Node;
private componentRef: ComponentRef<any>;
constructor(
@@ -95,7 +90,6 @@ export class PreviewExtensionComponent implements OnInit, OnChanges, OnDestroy {
if (this.componentRef && this.componentRef.instance) {
const instance = this.componentRef.instance;
instance.node = this.node;
instance.url = this.url;
instance.extension = this.extension;
}

View File

@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { FormRenderingService } from '@alfresco/adf-core';
import { FormFieldTypes, FormRenderingService } from '@alfresco/adf-core';
import { AttachFileCloudWidgetComponent } from './widgets/attach-file/attach-file-cloud-widget.component';
import { DropdownCloudWidgetComponent } from './widgets/dropdown/dropdown-cloud.widget';
import { DateCloudWidgetComponent } from './widgets/date/date-cloud.widget';
@@ -24,6 +24,7 @@ import { PeopleCloudWidgetComponent } from './widgets/people/people-cloud.widget
import { GroupCloudWidgetComponent } from './widgets/group/group-cloud.widget';
import { PropertiesViewerWidgetComponent } from './widgets/properties-viewer/properties-viewer.widget';
import { RadioButtonsCloudWidgetComponent } from './widgets/radio-buttons/radio-buttons-cloud.widget';
import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget';
@Injectable({
providedIn: 'root'
@@ -33,13 +34,14 @@ export class CloudFormRenderingService extends FormRenderingService {
super();
this.register({
upload: () => AttachFileCloudWidgetComponent,
dropdown: () => DropdownCloudWidgetComponent,
date: () => DateCloudWidgetComponent,
people: () => PeopleCloudWidgetComponent,
'functional-group': () => GroupCloudWidgetComponent,
'properties-viewer': () => PropertiesViewerWidgetComponent,
'radio-buttons': () => RadioButtonsCloudWidgetComponent
[FormFieldTypes.UPLOAD]: () => AttachFileCloudWidgetComponent,
[FormFieldTypes.DROPDOWN]: () => DropdownCloudWidgetComponent,
[FormFieldTypes.DATE]: () => DateCloudWidgetComponent,
[FormFieldTypes.PEOPLE]: () => PeopleCloudWidgetComponent,
[FormFieldTypes.FUNCTIONAL_GROUP]: () => GroupCloudWidgetComponent,
[FormFieldTypes.PROPERTIES_VIEWER]: () => PropertiesViewerWidgetComponent,
[FormFieldTypes.RADIO_BUTTONS]: () => RadioButtonsCloudWidgetComponent,
[FormFieldTypes.ALFRESCO_FILE_VIEWER]: () => FileViewerWidgetComponent
}, true);
}
}

View File

@@ -0,0 +1,7 @@
<div class="adf-file-viewer-widget {{field.className}}" [class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<adf-alfresco-viewer [overlayMode]="false" [nodeId]="field.value" [showViewer]="field.value" [allowGoBack]="false"></adf-alfresco-viewer>
<error-widget [error]="field.validationSummary"></error-widget>
</div>

View File

@@ -15,12 +15,10 @@
* limitations under the License.
*/
import { FormModel } from '../core/form.model';
import { TranslateModule } from '@ngx-translate/core';
import { FormFieldModel } from '../core/form-field.model';
import { FormService } from '../../../services/form.service';
import { FileViewerWidgetComponent } from './file-viewer.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormModel, FormService, FormFieldModel } from '@alfresco/adf-core';
describe('FileViewerWidgetComponent', () => {
const fakeForm = new FormModel();

View File

@@ -16,8 +16,7 @@
*/
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component';
import { WidgetComponent, FormService } from '@alfresco/adf-core';
/* eslint-disable @angular-eslint/component-selector */

View File

@@ -24,7 +24,7 @@ import { MaterialModule } from '../material.module';
import { FormCloudComponent } from './components/form-cloud.component';
import { FormDefinitionSelectorCloudComponent } from './components/form-definition-selector-cloud.component';
import { FormCustomOutcomesComponent } from './components/form-cloud-custom-outcomes.component';
import { ContentMetadataModule, ContentNodeSelectorModule, UploadModule } from '@alfresco/adf-content-services';
import { ContentMetadataModule, ContentModule, ContentNodeSelectorModule, UploadModule } from '@alfresco/adf-content-services';
import { DateCloudWidgetComponent } from './components/widgets/date/date-cloud.widget';
import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget';
@@ -39,6 +39,7 @@ import { PropertiesViewerWidgetComponent } from './components/widgets/properties
import { PropertiesViewerWrapperComponent } from './components/widgets/properties-viewer/properties-viewer-wrapper/properties-viewer-wrapper.component';
import { RadioButtonsCloudWidgetComponent } from './components/widgets/radio-buttons/radio-buttons-cloud.widget';
import { FilePropertiesTableCloudComponent } from './components/widgets/attach-file/file-properties-table-cloud.component';
import { FileViewerWidgetComponent } from './components/widgets/file-viewer/file-viewer.widget';
@NgModule({
imports: [
@@ -52,7 +53,8 @@ import { FilePropertiesTableCloudComponent } from './components/widgets/attach-f
PeopleCloudModule,
GroupCloudModule,
ContentMetadataModule,
UploadModule
UploadModule,
ContentModule
],
declarations: [
FormCloudComponent,
@@ -67,7 +69,8 @@ import { FilePropertiesTableCloudComponent } from './components/widgets/attach-f
GroupCloudWidgetComponent,
PropertiesViewerWrapperComponent,
PropertiesViewerWidgetComponent,
FilePropertiesTableCloudComponent
FilePropertiesTableCloudComponent,
FileViewerWidgetComponent
],
exports: [
FormCloudComponent,
@@ -80,7 +83,9 @@ import { FilePropertiesTableCloudComponent } from './components/widgets/attach-f
DateCloudWidgetComponent,
PeopleCloudWidgetComponent,
GroupCloudWidgetComponent,
PropertiesViewerWidgetComponent
PropertiesViewerWidgetComponent,
ContentModule,
FileViewerWidgetComponent
]
})
export class FormCloudModule {

View File

@@ -30,6 +30,7 @@ export * from './components/widgets/group/group-cloud.widget';
export * from './components/widgets/people/people-cloud.widget';
export * from './components/widgets/properties-viewer/properties-viewer.widget';
export * from './components/widgets/radio-buttons/radio-buttons-cloud.widget';
export * from './components/widgets/file-viewer/file-viewer.widget';
export * from './services/content-cloud-node-selector.service';
export * from './services/form-cloud.service';

View File

@@ -31,11 +31,14 @@ import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-butto
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DropdownWidgetComponent } from './widgets/dropdown/dropdown.widget';
import { DynamicTableModule } from './widgets/dynamic-table/dynamic-table.module';
import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget';
import { ContentModule } from '@alfresco/adf-content-services';
@NgModule({
imports: [
DynamicTableModule,
CoreModule,
ContentModule,
MaterialModule
],
declarations: [
@@ -50,7 +53,8 @@ import { DynamicTableModule } from './widgets/dynamic-table/dynamic-table.module
FormListComponent,
RadioButtonsWidgetComponent,
DropdownWidgetComponent,
TypeaheadWidgetComponent
TypeaheadWidgetComponent,
FileViewerWidgetComponent
],
exports: [
FormComponent,
@@ -61,7 +65,8 @@ import { DynamicTableModule } from './widgets/dynamic-table/dynamic-table.module
RadioButtonsWidgetComponent,
TypeaheadWidgetComponent,
DropdownWidgetComponent,
FormListComponent
FormListComponent,
FileViewerWidgetComponent
]
})
export class FormModule {

View File

@@ -26,6 +26,7 @@ import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-butto
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DocumentWidgetComponent } from './widgets/document/document.widget';
import { AttachFileWidgetComponent } from './widgets/content-widget/attach-file-widget.component';
import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget';
describe('ProcessFormRenderingService', () => {
@@ -100,4 +101,10 @@ describe('ProcessFormRenderingService', () => {
expect(type).toBe(DynamicTableWidgetComponent);
});
it('should resolve File Viewer widget for file viewer', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.ALFRESCO_FILE_VIEWER);
const type = resolver(null);
expect(type).toBe(FileViewerWidgetComponent);
});
});

View File

@@ -29,6 +29,7 @@ import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-butto
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DynamicTableWidgetComponent } from './widgets/dynamic-table/dynamic-table.widget';
import { DropdownWidgetComponent } from './widgets/dropdown/dropdown.widget';
import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget';
@Injectable({
providedIn: 'root'
@@ -46,7 +47,8 @@ export class ProcessFormRenderingService extends FormRenderingService {
[FormFieldTypes.DOCUMENT]: () => DocumentWidgetComponent,
[FormFieldTypes.PEOPLE]: () => PeopleWidgetComponent,
[FormFieldTypes.FUNCTIONAL_GROUP]: () => FunctionalGroupWidgetComponent,
[FormFieldTypes.DYNAMIC_TABLE]: () => DynamicTableWidgetComponent
[FormFieldTypes.DYNAMIC_TABLE]: () => DynamicTableWidgetComponent,
[FormFieldTypes.ALFRESCO_FILE_VIEWER]: () => FileViewerWidgetComponent
}, true);
}
}

View File

@@ -2,6 +2,6 @@
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<adf-viewer [overlayMode]="false" [nodeId]="field.value" [showViewer]="field.value" [allowGoBack]="false"></adf-viewer>
<adf-alfresco-viewer [nodeId]="field.value" [showViewer]="field.value" [allowGoBack]="false"></adf-alfresco-viewer>
<error-widget [error]="field.validationSummary"></error-widget>
</div>

View File

@@ -0,0 +1,19 @@
file-viewer-widget {
height: 100%;
width: 100%;
.adf-file-viewer-widget {
height: 100%;
width: 100%;
adf-viewer.adf-viewer {
position: relative;
.adf-viewer-container {
.adf-viewer-content > div {
height: 90vh;
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
/*!
* @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 { FileViewerWidgetComponent } from './file-viewer.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormModel, FormService, FormFieldModel } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
describe('FileViewerWidgetComponent', () => {
const fakeForm = new FormModel();
let widget: FileViewerWidgetComponent;
let formServiceStub: Partial<FormService>;
let fixture: ComponentFixture<FileViewerWidgetComponent>;
const fakePngAnswer: any = {
id: '1933',
link: false,
isExternal: false,
relatedContent: false,
contentAvailable: true,
name: 'a_png_file.png',
simpleType: 'image',
mimeType: 'image/png',
previewStatus: 'queued',
thumbnailStatus: 'queued',
created: '2022-10-14T17:17:37.099Z',
createdBy: { id: 1001, firstName: 'Admin', lastName: 'admin', email: 'admin@example.com' }
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
],
declarations: [ FileViewerWidgetComponent ],
providers: [ { provide: FormService, useValue: formServiceStub } ]
});
formServiceStub = TestBed.inject(FormService);
fixture = TestBed.createComponent(FileViewerWidgetComponent);
widget = fixture.componentInstance;
});
it('should set the file id corretly when the field value is an array', (done) => {
const fakeField = new FormFieldModel(fakeForm, { id: 'fakeField', value: [fakePngAnswer] });
widget.field = fakeField;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(widget.field.value).toBe('1933');
done();
});
});
it('should set the file id corretly when the field value is a string', (done) => {
const fakeField = new FormFieldModel(fakeForm, { id: 'fakeField', value: 'fakeValue' });
widget.field = fakeField;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(widget.field.value).toBe('fakeValue');
done();
});
});
});

View File

@@ -0,0 +1,54 @@
/*!
* @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 { FormService, WidgetComponent } from '@alfresco/adf-core';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
/* eslint-disable @angular-eslint/component-selector */
@Component({
selector: 'file-viewer-widget',
templateUrl: './file-viewer.widget.html',
styleUrls: ['./file-viewer.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class FileViewerWidgetComponent extends WidgetComponent implements OnInit {
constructor(formService: FormService) {
super(formService);
}
ngOnInit(): void {
if (this.field &&
this.field.value &&
Array.isArray(this.field.value) &&
this.field.value.length) {
const file = this.field.value[0];
this.field.value = file.id;
}
}
}

View File

@@ -22,6 +22,7 @@ export * from './radio-buttons/radio-buttons.widget';
export * from './functional-group/functional-group.widget';
export * from './typeahead/typeahead.widget';
export * from './dropdown/dropdown.widget';
export * from './file-viewer/file-viewer.widget';
export * from './dynamic-table/index';

View File

@@ -86,10 +86,10 @@ export class ViewerPage {
pdfPageLoaded = $('[data-page-number="1"][data-loaded="true"], adf-img-viewer, adf-txt-viewer');
downloadSwitch = $('#adf-switch-download');
downloadButton = $('#adf-viewer-download');
downloadButton = $('#adf-alfresco-viewer-download');
printSwitch = $('#adf-switch-print');
printButton = $('#adf-viewer-print');
printButton = $('#adf-alfresco-viewer-print');
allowSidebarSwitch = $('#adf-switch-allowsidebar');
allowLeftSidebarSwitch = $('#adf-switch-allowLeftSidebar');