From 4043d55fc41304f508bf62e74a65d959fcc0646c Mon Sep 17 00:00:00 2001 From: Eugenio Romano Date: Wed, 1 Feb 2023 17:25:43 +0100 Subject: [PATCH] [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 (https://github.com/Alfresco/alfresco-ng2-components/pull/7992/commits/9f21b6dc69e0cd044344b28c9e6c47d7a2d34f0c\#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 Co-authored-by: Amedeo Lepore --- .../cloud/cloud-viewer.component.html | 5 +- .../file-view/file-view.component.html | 725 +++++----- .../file-view/file-view.component.ts | 21 +- .../components/file-view/file-view.module.ts | 1 + .../overlay-viewer.component.html | 4 +- .../shared-link-view.component.html | 4 +- .../components/alfresco-viewer.component.md | 474 +++++++ .../components/document-list.component.md | 2 +- .../services/search-query-builder.service.md | 4 +- .../components/viewer-render.component.md | 275 ++++ docs/core/components/viewer.component.md | 158 +-- .../components/preview-extension.component.md | 1 - docs/upgrade-guide/upgrade50-60.md | 5 + .../viewer-content-services-component.e2e.ts | 1 - e2e/core/viewer/viewer-properties.e2e.ts | 13 +- .../src/lib/content.module.ts | 7 +- .../src/lib/viewer/alfresco-viewer.module.ts | 51 + .../components/alfresco-viewer.component.html | 71 + .../components/alfresco-viewer.component.scss | 1 + .../alfresco-viewer.component.spec.ts | 907 +++++++++++++ .../components/alfresco-viewer.component.ts | 451 ++++++ lib/content-services/src/lib/viewer/index.ts | 18 + .../src/lib/viewer/public-api.ts | 21 + .../services/rendition-viewer.service.ts | 306 +++++ lib/content-services/src/public-api.ts | 1 + .../base-viewer/base-viewer.widget.html | 7 + .../base-viewer/base-viewer.widget.scss | 19 + .../base-viewer/base-viewer.widget.spec.ts | 85 ++ .../widgets/base-viewer/base-viewer.widget.ts | 55 + .../widgets/core/form-field-types.ts | 4 +- .../src/lib/form/components/widgets/index.ts | 6 +- .../form/services/form-rendering.service.ts | 2 +- lib/core/src/lib/services/url.service.ts | 42 + .../components/img-viewer.component.html | 6 +- .../components/img-viewer.component.scss | 12 + .../components/img-viewer.component.spec.ts | 16 +- .../viewer/components/img-viewer.component.ts | 10 +- .../components/media-player.component.html | 2 +- .../components/media-player.component.ts | 20 +- .../components/pdf-viewer-host.component.scss | 2 - .../components/pdf-viewer.component.html | 2 +- .../components/pdf-viewer.component.scss | 57 +- .../viewer/components/pdf-viewer.component.ts | 2 +- .../components/viewer-render.component.html | 102 ++ .../components/viewer-render.component.scss | 69 + .../viewer-render.component.spec.ts | 445 ++++++ .../components/viewer-render.component.ts | 190 +++ ...viewer-toolbar-custom-actions.component.ts | 32 + .../viewer/components/viewer.component.html | 151 +-- .../viewer/components/viewer.component.scss | 74 - .../components/viewer.component.spec.ts | 1204 +++-------------- .../lib/viewer/components/viewer.component.ts | 686 ++-------- .../viewer-extension.directive.spec.ts | 4 +- .../directives/viewer-extension.directive.ts | 4 +- lib/core/src/lib/viewer/public-api.ts | 3 +- .../lib/viewer/services/view-util.service.ts | 358 ++--- lib/core/src/lib/viewer/viewer.module.ts | 16 +- .../viewer/preview-extension.component.ts | 6 - .../cloud-form-rendering.service.ts | 18 +- .../file-viewer/file-viewer.widget.html | 7 + .../file-viewer/file-viewer.widget.scss | 0 .../file-viewer/file-viewer.widget.spec.ts | 4 +- .../widgets/file-viewer/file-viewer.widget.ts | 3 +- .../src/lib/form/form-cloud.module.ts | 13 +- .../src/lib/form/public-api.ts | 1 + .../src/lib/form/form.module.ts | 9 +- .../process-form-rendering.service.spec.ts | 7 + .../form/process-form-rendering.service.ts | 4 +- .../file-viewer/file-viewer.widget.html | 2 +- .../file-viewer/file-viewer.widget.scss | 19 + .../file-viewer/file-viewer.widget.spec.ts | 81 ++ .../widgets/file-viewer/file-viewer.widget.ts | 54 + .../src/lib/form/widgets/public-api.ts | 1 + .../lib/protractor/core/pages/viewer.page.ts | 4 +- package.json | 3 +- tools/doc/mqDefs.js | 4 +- tools/doc/remarkGraphQl.js | 4 +- tools/doc/reviewChecker.js | 4 +- tools/doc/sourceInfoClasses.js | 14 +- tools/doc/tools/fileChecker.js | 14 +- tools/doc/tools/gqIndex.js | 4 +- tools/doc/tools/linkFixer.js | 8 +- tools/doc/tools/sourceLinker.js | 6 +- tools/doc/tools/tsInfo.js | 8 +- tools/doc/tools/typeLinker.js | 16 +- 85 files changed, 4803 insertions(+), 2729 deletions(-) create mode 100644 docs/content-services/components/alfresco-viewer.component.md create mode 100644 docs/core/components/viewer-render.component.md create mode 100644 lib/content-services/src/lib/viewer/alfresco-viewer.module.ts create mode 100644 lib/content-services/src/lib/viewer/components/alfresco-viewer.component.html create mode 100644 lib/content-services/src/lib/viewer/components/alfresco-viewer.component.scss create mode 100644 lib/content-services/src/lib/viewer/components/alfresco-viewer.component.spec.ts create mode 100644 lib/content-services/src/lib/viewer/components/alfresco-viewer.component.ts create mode 100644 lib/content-services/src/lib/viewer/index.ts create mode 100644 lib/content-services/src/lib/viewer/public-api.ts create mode 100644 lib/content-services/src/lib/viewer/services/rendition-viewer.service.ts create mode 100644 lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.html create mode 100644 lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.scss create mode 100644 lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.spec.ts create mode 100644 lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.ts create mode 100644 lib/core/src/lib/services/url.service.ts create mode 100644 lib/core/src/lib/viewer/components/viewer-render.component.html create mode 100644 lib/core/src/lib/viewer/components/viewer-render.component.scss create mode 100644 lib/core/src/lib/viewer/components/viewer-render.component.spec.ts create mode 100644 lib/core/src/lib/viewer/components/viewer-render.component.ts create mode 100644 lib/core/src/lib/viewer/components/viewer-toolbar-custom-actions.component.ts create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/file-viewer/file-viewer.widget.html rename lib/{core => process-services-cloud}/src/lib/form/components/widgets/file-viewer/file-viewer.widget.scss (100%) rename lib/{core => process-services-cloud}/src/lib/form/components/widgets/file-viewer/file-viewer.widget.spec.ts (94%) rename lib/{core => process-services-cloud}/src/lib/form/components/widgets/file-viewer/file-viewer.widget.ts (93%) rename lib/{core/src/lib/form/components => process-services/src/lib/form}/widgets/file-viewer/file-viewer.widget.html (74%) create mode 100644 lib/process-services/src/lib/form/widgets/file-viewer/file-viewer.widget.scss create mode 100644 lib/process-services/src/lib/form/widgets/file-viewer/file-viewer.widget.spec.ts create mode 100644 lib/process-services/src/lib/form/widgets/file-viewer/file-viewer.widget.ts diff --git a/demo-shell/src/app/components/cloud/cloud-viewer.component.html b/demo-shell/src/app/components/cloud/cloud-viewer.component.html index f224ceba15..567f80e01f 100644 --- a/demo-shell/src/app/components/cloud/cloud-viewer.component.html +++ b/demo-shell/src/app/components/cloud/cloud-viewer.component.html @@ -1,4 +1,3 @@ - - + diff --git a/demo-shell/src/app/components/file-view/file-view.component.html b/demo-shell/src/app/components/file-view/file-view.component.html index f627613bab..66345850a8 100644 --- a/demo-shell/src/app/components/file-view/file-view.component.html +++ b/demo-shell/src/app/components/file-view/file-view.component.html @@ -1,340 +1,382 @@ - + - - - - - - - - - - - - - -

- - Display Default Properties - -

- -

- - Display Empty Metadata - -

- -

- - Multi accordion - -

- -

- - Read Only - -

- -

- - Custom preset - -

- -

- - - - - - -

- -

- - - - - - - - -

- -
- - - - - - - - - -
-
- - - - - - -

- - Custom Name - -

- -

- - - - - -

- -

- - Url File - -

- -

- - - - - -

- -

- - Show Toolbar - -

- -

- - Allow GoBack - -

- -

- - Open With - -

- -

- - More Actions - -

- -

- - More Actions Menu - -

- -

- - Allow Download - -

- -

- - Allow Print - -

- -

- - Allow Right Sidebar - -

- -

- - Allow Left Sidebar - -

- -

- - Custom Toolbar - -

- -

- - Show tab with icon - -

- -

- - Show tab with icon and label - -

- -

- -

- -

- -

- -
- - - - - - -
-
- - + + +

My custom toolbar

+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +

+ + Display Default Properties + +

+ +

+ + Display Empty Metadata + +

+ +

+ + Multi accordion + +

+ +

+ + Read Only + +

+ +

+ + Custom preset + +

+ +

+ + + + + + +

+ +

+ + + + + + + + +

+ +
+ + + + + + + + + +
+
+ + + + + + +

+ + Url File + +

+ +

+ + + + + +

+ +

+ + Show Toolbar + +

+ +

+ + Allow GoBack + +

+ +

+ + Open With + +

+ +

+ + More Actions + +

+ +

+ + More Actions Menu + +

+ +

+ + Allow Download + +

+ +

+ + Allow Print + +

+ +

+ + Allow Right Sidebar + +

+ +

+ + Allow Left Sidebar + +

+ +

+ + Custom Toolbar + +

+ +

+ + Show tab with icon + +

+ +

+ + Show tab with icon and label + +

+ +

+ +

+ +

+ +

+ +
+ + + + + + +
+
+ + + @@ -386,36 +428,5 @@ - - - - - - -
diff --git a/demo-shell/src/app/components/file-view/file-view.component.ts b/demo-shell/src/app/components/file-view/file-view.component.ts index e8e4d75523..484a88abfb 100644 --- a/demo-shell/src/app/components/file-view/file-view.component.ts +++ b/demo-shell/src/app/components/file-view/file-view.component.ts @@ -45,7 +45,6 @@ export class FileViewComponent implements OnInit { customPreset: string = null; displayDefaultProperties = true; showToolbar = true; - displayName = null; urlFile = null; allowGoBack = true; openWith = false; @@ -55,7 +54,6 @@ export class FileViewComponent implements OnInit { allowLeftSidebar = true; moreActions = true; moreActionsMenu = false; - customName = false; fileUrlSwitch = false; showLeftSidebar = null; showRightSidebar = false; @@ -65,8 +63,9 @@ export class FileViewComponent implements OnInit { showTabWithIconAndLabel = false; desiredAspect: string = null; showAspect: string = null; - content: Blob; name: string; + fileName: string; + blobFile: Blob; constructor(private router: Router, private route: ActivatedRoute, @@ -93,9 +92,9 @@ export class FileViewComponent implements OnInit { }, () => this.router.navigate(['/files', id]) ); - } else if (this.preview.content) { - this.content = this.preview.content; - this.displayName = this.preview.name; + } else if (this.preview?.content) { + this.blobFile = this.preview.content; + this.fileName = this.preview.name; } }); } @@ -104,7 +103,7 @@ export class FileViewComponent implements OnInit { this.preview.showResource(this.nodeId, versionId); } - onViewerVisibilityChanged() { + onViewerClosed() { const primaryUrl = this.router.parseUrl(this.router.url).root.children[PRIMARY_OUTLET].toString(); this.router.navigateByUrl(primaryUrl); } @@ -182,14 +181,6 @@ export class FileViewComponent implements OnInit { this.showTabWithIconAndLabel = !this.showTabWithIconAndLabel; } - toggleCustomName() { - this.customName = !this.customName; - - if (!this.customName) { - this.displayName = null; - } - } - toggleFileUrl() { this.fileUrlSwitch = !this.fileUrlSwitch; diff --git a/demo-shell/src/app/components/file-view/file-view.module.ts b/demo-shell/src/app/components/file-view/file-view.module.ts index ce324a2756..ef3110df1d 100644 --- a/demo-shell/src/app/components/file-view/file-view.module.ts +++ b/demo-shell/src/app/components/file-view/file-view.module.ts @@ -34,6 +34,7 @@ const routes: Routes = [ CommonModule, RouterModule.forChild(routes), CoreModule, + ContentModule, InfoDrawerModule, ContentModule, ContentDirectiveModule, diff --git a/demo-shell/src/app/components/overlay-viewer/overlay-viewer.component.html b/demo-shell/src/app/components/overlay-viewer/overlay-viewer.component.html index 332442c891..f8a8306edb 100644 --- a/demo-shell/src/app/components/overlay-viewer/overlay-viewer.component.html +++ b/demo-shell/src/app/components/overlay-viewer/overlay-viewer.component.html @@ -15,8 +15,8 @@ (preview)="showPreview($event)"> - - + diff --git a/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html index 32110f9787..6483c22865 100644 --- a/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html +++ b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html @@ -1,7 +1,7 @@ - - + diff --git a/docs/content-services/components/alfresco-viewer.component.md b/docs/content-services/components/alfresco-viewer.component.md new file mode 100644 index 0000000000..f4ec65addd --- /dev/null +++ b/docs/content-services/components/alfresco-viewer.component.md @@ -0,0 +1,474 @@ +--- +Title: Alfresco Viewer component +Added: 6.0.0 +Status: Active +--- + +# [Alfresco Viewer component](../../../lib/content-services/src/lib/viewer/components/alfresco-viewer.component.ts "Defined in alfresco-viewer.component.ts") + +Displays content from an ACS repository. + +See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) + +## Contents + +- [Basic usage](#basic-usage) + - [Transclusions](#transclusions) +- [Class members](#class-members) + - [Properties](#properties) + - [Events](#events) +- [Keyboard shortcuts](#keyboard-shortcuts) +- [Details](#details) + - [Integrating with the Document List component](#integrating-with-the-document-list-component) + - [Supported file formats](#supported-file-formats) + - [Content Renditions](#content-renditions) + - [Configuring PDF.js library](#configuring-pdfjs-library) + - [Extending the Viewer](#extending-the-viewer) + - [Custom layout](#custom-layout) + - [Printing](#printing) +- [See also](#see-also) + +## Basic usage + +Using with node id: + +```html + + +``` + +Using with shared link: + +```html + + +``` + +Note that if you have a URL which contains a shared link ID, you should extract the +ID portion and use it with the `sharedLinkId` property rather than using the whole +URL with `urlFile`. + +### [Transclusions](../../user-guide/transclusion.md) + +The [Alfresco Viewer component](viewer.component.md) lets you transclude content for the toolbar (and toolbar buttons), +the sidebar, thumbnails, and the "Open with" and "More actions" menus. +See the [Custom layout](#custom-layout) section for full details of all available tranclusions. + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| allowDownload | `boolean` | true | Toggles downloading. | +| allowGoBack | `boolean` | true | Allows `back` navigation | +| allowLeftSidebar | `boolean` | false | Allow the left the sidebar. | +| allowNavigate | `boolean` | false | Toggles before/next navigation. You can use the arrow buttons to navigate between documents in the collection. | +| allowPrint | `boolean` | false | Toggles printing. | +| allowRightSidebar | `boolean` | false | Allow the right sidebar. | +| canNavigateBefore | `boolean` | true | Toggles the "before" ("<") button. Requires `allowNavigate` to be enabled. | +| canNavigateNext | `boolean` | true | Toggles the next (">") button. Requires `allowNavigate` to be enabled. | +| maxRetries | `number` | 30 | Number of times the Viewer will retry fetching content Rendition. There is a delay of at least one second between attempts. | +| nodeId | `string` | null | Node Id of the file to load. | +| overlayMode | `boolean` | false | If `true` then show the Viewer as a full page over the current content. Otherwise fit inside the parent div. | +| sharedLinkId | `string` | null | Shared link id (to display shared file). | +| showLeftSidebar | `boolean` | false | Toggles left sidebar visibility. Requires `allowLeftSidebar` to be set to `true`. | +| showRightSidebar | `boolean` | false | Toggles right sidebar visibility. Requires `allowRightSidebar` to be set to `true`. | +| showToolbar | `boolean` | true | Hide or show the toolbar | +| showViewer | `boolean` | true | Hide or show the viewer | +| sidebarLeftTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the left sidebar. The template context contains the loaded node data. | +| sidebarRightTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the right sidebar. The template context contains the loaded node data. | +| versionId | `string` | null | Version Id of the file to load. | + +### Events + +| Name | Type | Description | +| ---- | ---- | ----------- | +| close | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the viewer close | +| invalidSharedLink | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the shared link used is not valid. | +| navigateBefore | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when user clicks 'Navigate Before' ("<") button. | +| navigateNext | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when user clicks 'Navigate Next' (">") button. | + +## Keyboard shortcuts + +| Name | Description | +| ---- | ----------- | +| Esc | Close the viewer (overlay mode only). | +| Left | Invoke 'Navigate before' action. | +| Right | Invoke 'Navigate next' action. | +| Ctrl+F | Activate full-screen mode. | + +## Details + +### Integrating with the Document List component + +Below is the most simple integration of the Viewer and +[Document List](../../content-services/components/document-list.component.md) components within your custom component: + +```html + + + + + +``` + +The component controller class implementation might look like the following: + +```ts +export class OverlayViewerComponent { + + @Input() + showViewer: boolean = false; + + nodeId: string; + + showPreview(event) { + if (event.value.entry.isFile) { + this.nodeId = event.value.entry.id; + this.showViewer = true; + } + } +} +``` + +### Supported file formats + +The [Alfresco Viewer component](viewer.component.md) consists of separate Views that handle particular file types or type families based on either a file extension or a mime type: + +- PDF View + - application/pdf + - \*.pdf +- Image View + - image/png + - image/jpeg + - image/gif + - image/bmp + - image/svg+xml + - \*.png + - \*.jpg + - \*.jpeg + - \*.gif + - \*.bpm + - \*.svg +- Text View + - text/plain + - text/csv + - text/xml + - text/html + - application/x-javascript + - \*.txt + - \*.xml + - \*.js + - \*.html + - \*.json + - \*.ts +- Media View + - video/mp4 + - video/webm + - video/ogg + - audio/mpeg + - audio/ogg + - audio/wav + - \*.wav + - \*.mp4 + - \*.mp3 + - \*.webm + - \*.ogg + +### Content Renditions + +For those extensions and mime types that cannot be natively displayed in the browser, the Viewer will try to fetch the corresponding rendition using the [renditions service api](../services/renditions.service.md). + +For the full list of supported types please refer to: [File types that support preview and thumbnail generation](http://docs.alfresco.com/5.2/references/valid-transformations-preview.html). + +### Configuring PDF.js library + +Configure your webpack-enabled application with the PDF.js library as follows. + +1. Install pdfjs-dist + +```sh +npm install pdfjs-dist +``` + +2. Update `vendors.ts` by appending the following code. This will enable the [Alfresco Viewer component](viewer.component.md) + and compatibility mode for all browsers. It will also configure the web worker for the PDF.js + library to render PDF files in the background: + +```ts +// PDF.js +require('pdfjs-dist/web/compatibility.js'); +const pdfjsLib = require('pdfjs-dist'); +pdfjsLib.PDFJS.workerSrc = './pdf.worker.js'; +require('pdfjs-dist/web/pdf_viewer.js'); +``` + +3. Update the `plugins` section of the `webpack.common.js` file with the following lines: + +```js +new CopyWebpackPlugin([ + ... + { + from: 'node_modules/pdfjs-dist/build/pdf.worker.js', + to: 'pdf.worker.js' + } +]) +``` + +### Extending the Viewer + +#### Internal extension mechanism + +The Viewer supports dynamically-loaded viewer preview extensions, to know more about it [Preview Extension component](../../extensions/components/preview-extension.component.md). This + +#### Code extension mechanism] + +You can define your own custom handler to handle other file formats that are not yet supported by +the [Alfresco Viewer component](viewer.component.md). Below is an example that shows how to use the `adf-viewer-extension` +to handle 3D data files: + +```html + + + + + + + + + + +``` + +Note: you need to add the `ng2-3d-editor` dependency to your `package.json` file to make the example above work. + +You can define multiple `adf-viewer-extension` templates if required: + +```html + + + + + + + + + + + + + + + + +``` + +### Custom layout + +The [Alfresco Viewer Component](viewer.component.md) lets you transclude custom content in several different places as +explained in the sections below. + +#### Custom toolbar + +You can replace the standard viewer toolbar with your own custom implementation: + +```html + + +

toolbar

+
+
+``` + +Everything you put inside the "adf-alfresco-viewer-toolbar" tags will be rendered instead of the +standard toolbar. + +#### Custom toolbar buttons + +If you are happy with the custom toolbar's behaviour but want to add some extra buttons +then you can do so as shown in the following example: + +```html + + + + + + + +``` + +The result should look like this: + +![Custom Toolbar Actions](../../docassets/images/viewer-toolbar-actions.png) + +#### Custom sidebar + +The [Alfresco Viewer Component](viewer.component.md) also supports custom sidebar components and layouts. +Set the `allowRightSidebar` property to `true` to enable this feature. + +The custom sidebar can be injected in two different ways. The first way is to use +transclusion, which will display all content placed inside the `` element: + +```html + + +

My info

+
+
+``` + +The second way to customize the sidebar is to use template injection but note that this only works +when using the viewer with `nodeId`. + +```html + + + + +``` + +#### Custom thumbnails + +The PDF viewer comes with its own default list of thumbnails but you can replace this +by providing a custom template and binding to the context property `viewer` to access the PDFJS.PDFViewer +instance. + +![PDF thumbnails](../../docassets/images/pdf-thumbnails.png) + +Provide the custom template as in the following example: + +```javascript +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'custom-thumbnails', + template: '

Custom Thumbnails Component

' +}) +export class CustomThumbnailsComponent { + @Input() pdfViewer: any; + + ... +} +``` + +```html + + + + + +``` + +#### Custom "Open With" menu + +You can enable a custom "Open With" menu by providing at least one action inside the +`adf-alfresco-viewer-open-with` tag: + +```html + + + + + + + + + +``` + +![Open with](../../docassets/images/viewer-open-with.png) + +#### Custom "More actions" menu + +You can enable a custom "More actions" menu by providing at least one action inside the `adf-alfresco-viewer-more-actions` tag: + +```html + + + + + + + + + +``` + +![More actions](../../docassets/images/viewer-more-actions.png) + +#### Custom zoom scaling + +You can set a default zoom scaling value for pdf viewer by adding the following code in `app.config.json`. +Note: For the pdf viewer the value has to be within the range of 25 - 1000. + +"adf-alfresco-viewer": { +"pdf-viewer-scaling": 150 +} + +In the same way you can set a default zoom scaling value for the image viewer by adding the following code in `app.config.json`. + +"adf-alfresco-viewer": { +"image-viewer-scaling": 150 +} + +By default the viewer's zoom scaling is set to 100%. + +### Printing + +You can configure the Viewer to let the user print the displayed content. The +component will show a "Print" button if the `allowPrint` property is set to +true. + +```html + + ... + +``` + +You can also use the `print` event to get notification when the user prints some +content. + +## See also + +- [Document List component](../../content-services/components/document-list.component.md) +- [Preview Extension component](../../extensions/components/preview-extension.component.md) diff --git a/docs/content-services/components/document-list.component.md b/docs/content-services/components/document-list.component.md index c634c97481..ffaf249907 100644 --- a/docs/content-services/components/document-list.component.md +++ b/docs/content-services/components/document-list.component.md @@ -99,7 +99,7 @@ Displays the documents from a repository. | nodeClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user clicks a list node | | nodeDblClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user double-clicks a list node | | nodeSelected | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeEntry.md)`[]>` | Emitted when the node selection change | -| preview | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user acts upon files with either single or double click (depends on `navigation-mode`). Useful for integration with the [Viewer component](../../core/components/viewer.component.md). | +| preview | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user acts upon files with either single or double click (depends on `navigation-mode`). Useful for integration with the Viewer component. | | ready | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodePaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/NodePaging.md)`>` | Emitted when the Document List has loaded all items and is ready for use | ## Details diff --git a/docs/content-services/services/search-query-builder.service.md b/docs/content-services/services/search-query-builder.service.md index 923db7f037..702ac4a7b1 100644 --- a/docs/content-services/services/search-query-builder.service.md +++ b/docs/content-services/services/search-query-builder.service.md @@ -25,7 +25,7 @@ Stores information from all the custom search and faceted search widgets, compil - **Returns** `QueryBody` - The finished query - **execute**(queryBody?: `QueryBody`)
Builds and executes the current query. - - _queryBody:_ `QueryBody` - (Optional) + - _queryBody:_ `QueryBody` - (Optional) (Optional) - **getDefaultConfiguration**(): [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)`|undefined`
- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)`|undefined` - @@ -92,7 +92,7 @@ Stores information from all the custom search and faceted search widgets, compil - **update**(queryBody?: `QueryBody`)
Builds the current query and triggers the `updated` event. - - _queryBody:_ `QueryBody` - (Optional) + - _queryBody:_ `QueryBody` - (Optional) (Optional) - **updateSelectedConfiguration**(index: `number`)
- _index:_ `number` - diff --git a/docs/core/components/viewer-render.component.md b/docs/core/components/viewer-render.component.md new file mode 100644 index 0000000000..23c53aae2b --- /dev/null +++ b/docs/core/components/viewer-render.component.md @@ -0,0 +1,275 @@ +--- +Title: Viewer Render component +Added: v6.0.0 +Status: Active +Last reviewed: 2022-11-25 +--- + +# [Viewer render component](../../../lib/core/src/lib/viewer/components/viewer-render.component.ts "Defined in viewer-render.component.ts") + +Displays content from an ACS repository. + +See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) + +## Contents + +- [Basic usage](#basic-usage) +- [Class members](#class-members) + - [Properties](#properties) + - [Events](#events) +- [Keyboard shortcuts](#keyboard-shortcuts) +- [Details](#details) + - [Custom file parameters](#custom-file-parameters) + - [Supported file formats](#supported-file-formats) + - [Configuring PDF.js library](#configuring-pdfjs-library) + - [Extending the Viewer](#extending-the-viewer) + - [Custom layout](#custom-layout) +- [See also](#see-also) + +## Basic usage + +Using with file url: + +```html + + +``` + +Using with file [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob): + +```html + + +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| allowFullScreen | `boolean` | true | Toggles the 'Full Screen' feature. | +| allowThumbnails | `boolean` | true | Toggles PDF thumbnails. | +| blobFile | [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) | | Loads a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) File | +| fileName | `string` | | Override Content filename. | +| isLoading | `boolean` | false | Override loading status | +| mimeType | `string` | | MIME type of the file content (when not determined by the filename extension). | +| readOnly | `boolean` | true | Enable when where is possible the editing functionalities | +| thumbnailsTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the pdf thumbnails. | +| tracks | [`Track`](../../../lib/core/src/lib/viewer/models/viewer.model.ts)`[]` | \[] | media subtitles for the media player | +| urlFile | `string` | "" | 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. | +| viewerType | `string` | "unknown" | Override Content view type. Viewer to use with the `urlFile` address (`pdf`, `image`, `media`, `text`). | + +### Events + +| Name | Type | Description | +| ---- | ---- | ----------- | +| close | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the img is submitted in the img viewer. | +| extensionChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the filename extension changes. | +| submitFile | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)`>` | Emitted when the img is submitted in the img viewer. | + +## Keyboard shortcuts + +| Name | Description | +| ---- | ----------- | +| Esc | Close the viewer (overlay mode only). | +| Left | Invoke 'Navigate before' action. | +| Right | Invoke 'Navigate next' action. | +| Ctrl+F | Activate full-screen mode. | + +## Details + +The component controller class implementation might look like the following: + +```ts +export class OverlayViewerComponent { + + @Input() + showViewer: boolean = false; + + nodeId: string; + + showPreview(event) { + if (event.value.entry.isFile) { + this.nodeId = event.value.entry.id; + this.showViewer = true; + } + } +} +``` + +### Custom file parameters + +You can provide custom file parameters, for example a value for the `mimeType` or `displayName` when using URL values that have no file names or extensions: + +```html + + +``` + +### Supported file formats + +The [Viewer render component](viewer.component.md) consists of separate Views that handle particular file types or type families based on either a file extension or a mime type: + +- PDF View + - application/pdf + - \*.pdf +- Image View + - image/png + - image/jpeg + - image/gif + - image/bmp + - image/svg+xml + - \*.png + - \*.jpg + - \*.jpeg + - \*.gif + - \*.bpm + - \*.svg +- Text View + - text/plain + - text/csv + - text/xml + - text/html + - application/x-javascript + - \*.txt + - \*.xml + - \*.js + - \*.html + - \*.json + - \*.ts +- Media View + - video/mp4 + - video/webm + - video/ogg + - audio/mpeg + - audio/ogg + - audio/wav + - \*.wav + - \*.mp4 + - \*.mp3 + - \*.webm + - \*.ogg + +### Configuring PDF.js library + +Configure your webpack-enabled application with the PDF.js library as follows. + +1. Install pdfjs-dist + +```sh +npm install pdfjs-dist +``` + +2. Update `vendors.ts` by appending the following code. This will enable the [Viewer render component](viewer.component.md) + and compatibility mode for all browsers. It will also configure the web worker for the PDF.js + library to render PDF files in the background: + +```ts +// PDF.js +require('pdfjs-dist/web/compatibility.js'); +const pdfjsLib = require('pdfjs-dist'); +pdfjsLib.PDFJS.workerSrc = './pdf.worker.js'; +require('pdfjs-dist/web/pdf_viewer.js'); +``` + +3. Update the `plugins` section of the `webpack.common.js` file with the following lines: + +```js +new CopyWebpackPlugin([ + ... + { + from: 'node_modules/pdfjs-dist/build/pdf.worker.js', + to: 'pdf.worker.js' + } +]) +``` + +The [Viewer render component](viewer.component.md) should now be able to display PDF files. + +### Extending the Viewer + +#### Internal extension mechanism + +The Viewer supports dynamically-loaded viewer preview extensions, to know more about it [Preview Extension component](../../extensions/components/preview-extension.component.md). This + +#### Code extension mechanism] + +You can define your own custom handler to handle other file formats that are not yet supported by +the [Viewer render component](viewer.component.md). Below is an example that shows how to use the `adf-viewer-render-extension` +to handle 3D data files: + +```html + + + + + + + + + + +``` + +Note: you need to add the `ng2-3d-editor` dependency to your `package.json` file to make the example above work. + +You can define multiple `adf-viewer-render-extension` templates if required: + +```html + + + + + + + + + + + + + + + + +``` + +### Custom layout + +The [Viewer render component](viewer.component.md) lets you transclude custom content in several different places as +explained in the sections below. + +#### Custom zoom scaling + +You can set a default zoom scaling value for pdf viewer by adding the following code in `app.config.json`. +Note: For the pdf viewer the value has to be within the range of 25 - 1000. + +"adf-viewer-render": { +"pdf-viewer-scaling": 150 +} + +In the same way you can set a default zoom scaling value for the image viewer by adding the following code in `app.config.json`. + +"adf-viewer-render": { +"image-viewer-scaling": 150 +} + +By default the viewer's zoom scaling is set to 100%. + +## See also + +- [Alfresco Viewer component](../../content-services/components/document-list.component.md) +- [Viewer Render component](../../core/components/viewer-render.component.md) diff --git a/docs/core/components/viewer.component.md b/docs/core/components/viewer.component.md index bab4a3834a..3697ee4424 100644 --- a/docs/core/components/viewer.component.md +++ b/docs/core/components/viewer.component.md @@ -2,14 +2,13 @@ Title: Viewer component Added: v2.0.0 Status: Active -Last reviewed: 2019-03-25 +Last reviewed: 2023-01-30 --- # [Viewer component](../../../lib/core/src/lib/viewer/components/viewer.component.ts "Defined in viewer.component.ts") -Displays content from an ACS repository. +Displays content from blob file or url file. -See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) ## Contents @@ -23,22 +22,21 @@ See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) - [Integrating with the Document List component](#integrating-with-the-document-list-component) - [Custom file parameters](#custom-file-parameters) - [Supported file formats](#supported-file-formats) - - [Content Renditions](#content-renditions) - [Configuring PDF.js library](#configuring-pdfjs-library) - [Extending the Viewer](#extending-the-viewer) - [Custom layout](#custom-layout) - - [Printing](#printing) - [See also](#see-also) ## Basic usage -Using with node id: +Using with blob file: ```html + [blobFile]="blobFile" + [fileName]="'filename.pdf'"> ``` @@ -47,23 +45,10 @@ Using with file url: ```html + [urlFile]="'https://fileUrl.com/filename.pdf'"> ``` -Using with shared link: - -```html - - -``` - -Note that if you have a URL which contains a shared link ID, you should extract the -ID portion and use it with the `sharedLinkId` property rather than using the whole -URL with `urlFile`. - ### [Transclusions](../../user-guide/transclusion.md) The [Viewer component](viewer.component.md) lets you transclude content for the toolbar (and toolbar buttons), @@ -81,41 +66,30 @@ See the [Custom layout](#custom-layout) section for full details of all availabl | allowGoBack | `boolean` | true | Allows `back` navigation | | allowLeftSidebar | `boolean` | false | Allow the left the sidebar. | | allowNavigate | `boolean` | false | Toggles before/next navigation. You can use the arrow buttons to navigate between documents in the collection. | -| allowPrint | `boolean` | false | Toggles printing. | | allowRightSidebar | `boolean` | false | Allow the right sidebar. | -| allowThumbnails | `boolean` | true | Toggles PDF thumbnails. | | blobFile | [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) | | Loads a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) File | | canNavigateBefore | `boolean` | true | Toggles the "before" ("<") button. Requires `allowNavigate` to be enabled. | | canNavigateNext | `boolean` | true | Toggles the next (">") button. Requires `allowNavigate` to be enabled. | -| displayName | `string` | | Specifies the name of the file when it is not available from the URL. | | fileName | `string` | | Content filename. | -| maxRetries | `number` | 30 | Number of times the Viewer will retry fetching content Rendition. There is a delay of at least one second between attempts. | | mimeType | `string` | | MIME type of the file content (when not determined by the filename extension). | -| nodeId | `string` | null | Node Id of the file to load. | | overlayMode | `boolean` | false | If `true` then show the Viewer as a full page over the current content. Otherwise fit inside the parent div. | -| sharedLinkId | `string` | null | Shared link id (to display shared file). | | showLeftSidebar | `boolean` | false | Toggles left sidebar visibility. Requires `allowLeftSidebar` to be set to `true`. | | showRightSidebar | `boolean` | false | Toggles right sidebar visibility. Requires `allowRightSidebar` to be set to `true`. | | showToolbar | `boolean` | true | Hide or show the toolbar | | showViewer | `boolean` | true | Hide or show the viewer | -| sidebarLeftTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the left sidebar. The template context contains the loaded node data. | -| sidebarRightTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the right sidebar. The template context contains the loaded node data. | -| thumbnailsTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the pdf thumbnails. | +| sidebarLeftTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the left sidebar. The template context contains the loaded data. | +| sidebarRightTemplate | [`TemplateRef`](https://angular.io/api/core/TemplateRef)`` | null | The template for the right sidebar. The template context contains the loaded data. | +| tracks | [`Track`](../../../lib/core/src/lib/viewer/models/viewer.model.ts)`[]` | \[] | media subtitles for the media player | | urlFile | `string` | "" | 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. | -| urlFileViewer | `string` | null | Viewer to use with the `urlFile` address (`pdf`, `image`, `media`, `text`). Used when `urlFile` has no filename and extension. | -| versionId | `string` | null | Version Id of the file to load. | ### Events | Name | Type | Description | | ---- | ---- | ----------- | -| extensionChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the filename extension changes. | -| goBack | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`BaseEvent`](../../../lib/core/src/lib/events/base.event.ts)`>` | Emitted when user clicks the 'Back' button. | -| invalidSharedLink | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the shared link used is not valid. | | navigateBefore | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when user clicks 'Navigate Before' ("<") button. | | navigateNext | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when user clicks 'Navigate Next' (">") button. | -| print | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`BaseEvent`](../../../lib/core/src/lib/events/base.event.ts)`>` | Emitted when user clicks the 'Print' button. | | showViewerChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the viewer is shown or hidden. | +| submitFile | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)`>` | Emitted when the img is submitted in the img viewer. | ## Keyboard shortcuts @@ -128,50 +102,13 @@ See the [Custom layout](#custom-layout) section for full details of all availabl ## Details -### Integrating with the Document List component - -Below is the most simple integration of the Viewer and -[Document List](../../content-services/components/document-list.component.md) components within your custom component: - -```html - - - - - -``` - -The component controller class implementation might look like the following: - -```ts -export class OverlayViewerComponent { - - @Input() - showViewer: boolean = false; - - nodeId: string; - - showPreview(event) { - if (event.value.entry.isFile) { - this.nodeId = event.value.entry.id; - this.showViewer = true; - } - } -} -``` - ### Custom file parameters -You can provide custom file parameters, for example a value for the `mimeType` or `displayName` when using URL values that have no file names or extensions: +You can provide custom file parameters, for example a value for the `mimeType` or `fileName` when using URL values that have no file names or extensions: ```html @@ -222,12 +159,6 @@ The [Viewer component](viewer.component.md) consists of separate Views that hand - \*.webm - \*.ogg -### Content Renditions - -For those extensions and mime types that cannot be natively displayed in the browser, the Viewer will try to fetch the corresponding rendition using the [renditions service api](../services/renditions.service.md). - -For the full list of supported types please refer to: [File types that support preview and thumbnail generation](http://docs.alfresco.com/5.2/references/valid-transformations-preview.html). - ### Configuring PDF.js library Configure your webpack-enabled application with the PDF.js library as follows. @@ -277,7 +208,7 @@ the [Viewer component](viewer.component.md). Below is an example that shows how to handle 3D data files: ```html - + @@ -296,7 +227,7 @@ Note: you need to add the `ng2-3d-editor` dependency to your `package.json` file You can define multiple `adf-viewer-extension` templates if required: ```html - + @@ -377,55 +308,13 @@ transclusion, which will display all content placed inside the ` ``` -The second way to customize the sidebar is to use template injection but note that this only works -when using the viewer with `nodeId`. - -```html - - - - -``` - -#### Custom thumbnails - -The PDF viewer comes with its own default list of thumbnails but you can replace this -by providing a custom template and binding to the context property `viewer` to access the PDFJS.PDFViewer -instance. - -![PDF thumbnails](../../docassets/images/pdf-thumbnails.png) - -Provide the custom template as in the following example: - -```javascript -import { Component, Input } from '@angular/core'; - -@Component({ - selector: 'custom-thumbnails', - template: '

Custom Thumbnails Component

' -}) -export class CustomThumbnailsComponent { - @Input() pdfViewer: any; - - ... -} -``` - -```html - - - - - -``` - #### Custom "Open With" menu You can enable a custom "Open With" menu by providing at least one action inside the `adf-viewer-open-with` tag: ```html - + + + + + diff --git a/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.scss b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.scss new file mode 100644 index 0000000000..b3c58707ac --- /dev/null +++ b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.scss @@ -0,0 +1 @@ +/* stylelint-disable-next-line no-empty-source */ diff --git a/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.spec.ts b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.spec.ts new file mode 100644 index 0000000000..ba553d06c6 --- /dev/null +++ b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.spec.ts @@ -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: ` + + +
+
+
+ ` +}) +class ViewerWithCustomToolbarComponent { +} + +@Component({ + selector: 'adf-viewer-container-toolbar-actions', + template: ` + + + + + + ` +}) +class ViewerWithCustomToolbarActionsComponent { +} + +@Component({ + selector: 'adf-viewer-container-sidebar', + template: ` + + +
+
+
+ ` +}) +class ViewerWithCustomSidebarComponent { +} + +@Component({ + selector: 'adf-dialog-dummy', + template: `` +}) +class DummyDialogComponent { +} + +@Component({ + selector: 'adf-viewer-container-open-with', + template: ` + + + + + + + + ` +}) +class ViewerWithCustomOpenWithComponent { +} + +@Component({ + selector: 'adf-viewer-container-more-actions', + template: ` + + + + + + + + ` +}) +class ViewerWithCustomMoreActionsComponent { +} + + +describe('AlfrescoViewerComponent', () => { + + let component: AlfrescoViewerComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + let 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('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).not.toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should provide tooltip for next file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = false; + component.canNavigateNext = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton.title).toBe('ADF_VIEWER.ACTIONS.NEXT_FILE'); + }); + + it('should show only previous file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).not.toBeNull(); + }); + + it('should provide tooltip for the previous file button', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton.title).toBe('ADF_VIEWER.ACTIONS.PREV_FILE'); + }); + + it('should show both file navigation buttons', async () => { + component.allowNavigate = true; + component.canNavigateBefore = true; + component.canNavigateNext = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).not.toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).not.toBeNull(); + }); + + it('should not show navigation buttons', async () => { + component.allowNavigate = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should now show navigation buttons even if navigation enabled', async () => { + component.allowNavigate = true; + component.canNavigateBefore = false; + component.canNavigateNext = false; + + fixture.detectChanges(); + await fixture.whenStable(); + + const nextButton = element.querySelector('[data-automation-id="adf-toolbar-next-file"]'); + expect(nextButton).toBeNull(); + + const prevButton = element.querySelector('[data-automation-id="adf-toolbar-pref-file"]'); + expect(prevButton).toBeNull(); + }); + + it('should render fullscreen button', () => { + expect(element.querySelector('[data-automation-id="adf-toolbar-fullscreen"]')).toBeDefined(); + }); + + it('should render default download button', (done) => { + component.allowDownload = true; + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-download"]')).toBeDefined(); + done(); + }); + }); + + it('should not render default download button', (done) => { + component.allowDownload = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-download"]')).toBeNull(); + done(); + }); + }); + + it('should render default print button', (done) => { + component.allowPrint = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-print"]')).toBeDefined(); + done(); + }); + }); + + it('should not render default print button', (done) => { + component.allowPrint = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(element.querySelector('[data-automation-id="adf-toolbar-print"]')).toBeNull(); + done(); + }); + }); + + it('should invoke print action with the toolbar button', (done) => { + component.allowPrint = true; + fixture.detectChanges(); + + spyOn(component, 'onPrintContent').and.stub(); + + const button: HTMLButtonElement = element.querySelector('[data-automation-id="adf-toolbar-print"]') as HTMLButtonElement; + button.click(); + + fixture.whenStable().then(() => { + expect(component.onPrintContent).toHaveBeenCalled(); + done(); + }); + }); + + it('should get and assign node for download', (done) => { + component.nodeId = '12'; + const displayName = 'the-name'; + const nodeDetails = { + entry: {name: displayName, id: '12', content: {mimeType: 'txt'}} + }; + + const contentUrl = '/content/url/path'; + + const node = new NodeEntry(nodeDetails); + + spyOn(component['nodesApi'], 'getNode').and.returnValue(Promise.resolve(node)); + spyOn(component['contentApi'], 'getContentUrl').and.returnValue(contentUrl); + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.nodeEntry).toBe(node); + done(); + }); + }); + + it('should render close viewer button if it is not a shared link', (done) => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).toBeDefined(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).not.toBeNull(); + done(); + }); + }); + + it('should not render close viewer button if it is a shared link', (done) => { + spyOn(component['sharedLinksApi'], 'getSharedLink') + .and.returnValue(Promise.reject({})); + + component.sharedLinkId = 'the-Shared-Link-id'; + component.mimeType = null; + + component.ngOnChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(element.querySelector('[data-automation-id="adf-toolbar-back"]')).toBeNull(); + done(); + }); + }); + + }); + + describe('Base component', () => { + + beforeEach(() => { + component.mimeType = 'application/pdf'; + component.nodeId = 'id1'; + + fixture.detectChanges(); + }); + + describe('SideBar Test', () => { + + it('should NOT display sidebar if is not allowed', (done) => { + component.showRightSidebar = true; + component.allowRightSidebar = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-right-sidebar'); + expect(sidebar).toBeNull(); + done(); + }); + }); + + it('should display sidebar on the right side', (done) => { + component.allowRightSidebar = true; + component.showRightSidebar = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-right-sidebar'); + expect(getComputedStyle(sidebar).order).toEqual('4'); + done(); + }); + }); + + it('should NOT display left sidebar if is not allowed', (done) => { + component.showLeftSidebar = true; + component.allowLeftSidebar = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-left-sidebar'); + expect(sidebar).toBeNull(); + done(); + }); + + }); + + it('should display sidebar on the left side', (done) => { + component.allowLeftSidebar = true; + component.showLeftSidebar = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const sidebar = element.querySelector('#adf-left-sidebar'); + expect(getComputedStyle(sidebar).order).toEqual('1'); + done(); + }); + }); + }); + + describe('View', () => { + + describe('Overlay mode true', () => { + + beforeEach(() => { + component.overlayMode = true; + component.fileName = 'fake-test-file.pdf'; + fixture.detectChanges(); + }); + + it('should header be present if is overlay mode', () => { + expect(element.querySelector('.adf-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(); + }); + }); + + }); +}); diff --git a/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.ts b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.ts new file mode 100644 index 0000000000..e8f2545479 --- /dev/null +++ b/lib/content-services/src/lib/viewer/components/alfresco-viewer.component.ts @@ -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 = null; + + /** The template for the left sidebar. The template context contains the loaded node data. */ + @Input() + sidebarLeftTemplate: TemplateRef = null; + + /** Emitted when the shared link used is not valid. */ + @Output() + invalidSharedLink = new EventEmitter(); + + /** Emitted when user clicks 'Navigate Before' ("<") button. */ + @Output() + navigateBefore = new EventEmitter(); + + /** Emitted when user clicks 'Navigate Next' (">") button. */ + @Output() + navigateNext = new EventEmitter(); + + /** Emitted when the viewer close */ + @Output() + showViewerChange = new EventEmitter(); + + private onDestroy$ = new Subject(); + + 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 { + + 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(); + } + +} diff --git a/lib/content-services/src/lib/viewer/index.ts b/lib/content-services/src/lib/viewer/index.ts new file mode 100644 index 0000000000..a7e30cc675 --- /dev/null +++ b/lib/content-services/src/lib/viewer/index.ts @@ -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'; diff --git a/lib/content-services/src/lib/viewer/public-api.ts b/lib/content-services/src/lib/viewer/public-api.ts new file mode 100644 index 0000000000..da745e3b45 --- /dev/null +++ b/lib/content-services/src/lib/viewer/public-api.ts @@ -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'; diff --git a/lib/content-services/src/lib/viewer/services/rendition-viewer.service.ts b/lib/content-services/src/lib/viewer/services/rendition-viewer.service.ts new file mode 100644 index 0000000000..a9b576c51d --- /dev/null +++ b/lib/content-services/src/lib/viewer/services/rendition-viewer.service.ts @@ -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 { + 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 { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async getRendition(nodeId: string, renditionId: string): Promise { + 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((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 { + 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 { + let currentRetry: number = 0; + return new Promise((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 { + + const url = versionId ? this.contentApi.getVersionRenditionUrl(nodeId, versionId, renditionId) : + this.contentApi.getRenditionUrl(nodeId, renditionId); + + return url; + } + + async generateMediaTracksRendition(nodeId: string): Promise { + 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 { + 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); + }); + } +} diff --git a/lib/content-services/src/public-api.ts b/lib/content-services/src/public-api.ts index f9dc600202..aa2e8467fe 100644 --- a/lib/content-services/src/public-api.ts +++ b/lib/content-services/src/public-api.ts @@ -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'; diff --git a/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.html b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.html new file mode 100644 index 0000000000..8657e32aad --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.html @@ -0,0 +1,7 @@ +
+ + + +
diff --git a/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.scss b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.scss new file mode 100644 index 0000000000..b108c3a176 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.scss @@ -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; + } + } + } + } +} diff --git a/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.spec.ts b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.spec.ts new file mode 100644 index 0000000000..331a32c1fb --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.spec.ts @@ -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; + let fixture: ComponentFixture; + + 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(); + }); + }); +}); diff --git a/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.ts b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.ts new file mode 100644 index 0000000000..2bc6a9c281 --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/base-viewer/base-viewer.widget.ts @@ -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; + } + } +} diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts index 8b7a697bb4..924e6a1b46 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts @@ -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, diff --git a/lib/core/src/lib/form/components/widgets/index.ts b/lib/core/src/lib/form/components/widgets/index.ts index c51b9d3b37..9001811e8c 100644 --- a/lib/core/src/lib/form/components/widgets/index.ts +++ b/lib/core/src/lib/form/components/widgets/index.ts @@ -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 ]; diff --git a/lib/core/src/lib/form/services/form-rendering.service.ts b/lib/core/src/lib/form/services/form-rendering.service.ts index 693bed6b06..cae3cd7a32 100644 --- a/lib/core/src/lib/form/services/form-rendering.service.ts +++ b/lib/core/src/lib/form/services/form-rendering.service.ts @@ -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) }; } diff --git a/lib/core/src/lib/services/url.service.ts b/lib/core/src/lib/services/url.service.ts new file mode 100644 index 0000000000..6d94e52034 --- /dev/null +++ b/lib/core/src/lib/services/url.service.ts @@ -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; + } + + +} diff --git a/lib/core/src/lib/viewer/components/img-viewer.component.html b/lib/core/src/lib/viewer/components/img-viewer.component.html index 96d01c06c0..42248d27de 100644 --- a/lib/core/src/lib/viewer/components/img-viewer.component.html +++ b/lib/core/src/lib/viewer/components/img-viewer.component.html @@ -1,5 +1,5 @@ -