diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index dca06a5c49..f055130a77 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -1476,5 +1476,9 @@ "ai:labels", "ai:textLines" ] + }, + "viewer": { + "enableFileAutoDownload": true, + "fileAutoDownloadSizeThresholdInMB": 15 } } diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 34c8d7a02c..a1de8a994f 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -653,6 +653,18 @@ +
+ + Enable FileAutoDownload + +
+
+ + + +
+
Upload
diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts index 8be126f59b..6980dcfa62 100644 --- a/demo-shell/src/app/components/files/files.component.ts +++ b/demo-shell/src/app/components/files/files.component.ts @@ -251,6 +251,9 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { selectedNodes = []; + enableFileAutoDownload: boolean = this.appConfig.get('viewer.enableFileAutoDownload', true); + fileAutoDownloadSizeThresholdInMB: number = this.appConfig.get('viewer.fileAutoDownloadSizeThresholdInMB', 15); + constructor(private notificationService: NotificationService, private uploadService: UploadService, private contentService: ContentService, @@ -777,4 +780,14 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { this.selectedNodes = []; } + onEnableFileAutoDownloadChange() { + const previewConfig = this.appConfig?.config['viewer']; + previewConfig['enableFileAutoDownload'] = this.enableFileAutoDownload; + } + + onFileAutoDownloadSizeThresholdChange() { + const previewConfig = this.appConfig?.config['viewer']; + previewConfig['fileAutoDownloadSizeThresholdInMB'] = this.fileAutoDownloadSizeThresholdInMB; + } + } diff --git a/docs/content-services/components/document-list.component.md b/docs/content-services/components/document-list.component.md index c634c97481..baabbfb7c7 100644 --- a/docs/content-services/components/document-list.component.md +++ b/docs/content-services/components/document-list.component.md @@ -790,6 +790,27 @@ This will give the following output: ![Custom loading](../../docassets/images/custom-loading.png) +### File Auto downloading + +In case of files exceeding a predefined file size, the document list component can be configured to automatically download those file when trying to preview them. +This can help in reducing server load, and ensuring quick access to such files. After turning this feature on, whenever the user tries to preview a file with a large +file size, the Document List component will first preview a dialog, asking for confirmation from the user on whether they want to download the file, or cancel the preview altogether. + +In order to configure the Document List to automatically download the files, the following environment variables would need to be set up in app.config.json - + +``` +"viewer": { + "enableFileAutoDownload": true, + "fileAutoDownloadSizeThresholdInMB": 15 +} +``` + +Here, `"enableFileAutoDownload": true,` would enable the file auto download feature on the Document List component. Setting this flag to false disables this feature, and always +triggers a file preview when trying to view a file, regardless of its size. + +The second configuration here, `"fileAutoDownloadSizeThresholdInMB": 15` specifies the file size threshold (in MB), after which the Document List component will start downloading the file. +In the example provided above, any file greater than 15MB in size would trigger the auto download functionality. Files lower than 15MB in size would continue to preview normally. + ## See also - [Datatable component](../../core/components/datatable.component.md) diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts index 124eb7e04b..a958a45ad2 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts @@ -27,7 +27,7 @@ import { DataTableModule, ObjectDataTableAdapter, ShowHeaderMode, - ThumbnailService + ThumbnailService, AppConfigService } from '@alfresco/adf-core'; import { ContentService } from '../../common/services/content.service'; @@ -62,6 +62,12 @@ import { ShareDataRow } from '../data/share-data-row.model'; import { DocumentLoaderNode } from '../models/document-folder.model'; import { matIconRegistryMock } from '../../testing/mat-icon-registry-mock'; import { domSanitizerMock } from '../../testing/dom-sanitizer-mock'; +import { MatDialog } from '@angular/material/dialog'; +import { FileAutoDownloadComponent } from './file-auto-download/file-auto-download.component'; + +const mockDialog = { + open: jasmine.createSpy('open') +}; describe('DocumentList', () => { @@ -71,6 +77,7 @@ describe('DocumentList', () => { let customResourcesService: CustomResourcesService; let thumbnailService: ThumbnailService; let contentService: ContentService; + let appConfigService: AppConfigService; let fixture: ComponentFixture; let element: HTMLElement; let eventMock: any; @@ -84,7 +91,10 @@ describe('DocumentList', () => { TranslateModule.forRoot(), ContentTestingModule ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { provide: MatDialog, useValue: mockDialog } + ] }); beforeEach(() => { @@ -102,6 +112,7 @@ describe('DocumentList', () => { customResourcesService = TestBed.inject(CustomResourcesService); thumbnailService = TestBed.inject(ThumbnailService); contentService = TestBed.inject(ContentService); + appConfigService = TestBed.inject(AppConfigService); spyFolder = spyOn(documentListService, 'getFolder').and.returnValue(of({ list: {} })); spyFolderNode = spyOn(documentListService, 'getFolderNode').and.returnValue(of(new NodeEntry({ entry: {} }))); @@ -1567,6 +1578,30 @@ describe('DocumentList', () => { }), undefined); }); + it('should display fileAutoDownload dialog if node size exceeds appConfig.viewer.fileAutoDownloadSizeThresholdInMB', async () => { + appConfigService.config = { + ...appConfigService.config, + 'viewer': { + 'enableFileAutoDownload': true, + 'fileAutoDownloadSizeThresholdInMB': 10 + } + }; + documentList.navigationMode = DocumentListComponent.SINGLE_CLICK_NAVIGATION; + const node = { entry: { + ...mockNode1, + content: { + ...mockNode1.content, + sizeInBytes: 104857600 + } + } }; + documentList.onNodeClick(node); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockDialog.open).toHaveBeenCalledWith(FileAutoDownloadComponent, { disableClose: true, data: node }); + }); + describe('Preselect nodes', () => { beforeEach(() => { diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.ts b/lib/content-services/src/lib/document-list/components/document-list.component.ts index 4be8ddbb66..c4947d30dd 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.ts @@ -66,6 +66,10 @@ import { LockService } from '../services/lock.service'; import { DocumentLoaderNode } from '../models/document-folder.model'; import { takeUntil } from 'rxjs/operators'; import { ADF_DOCUMENT_PARENT_COMPONENT } from './document-list.token'; +import { MatDialog } from '@angular/material/dialog'; +import { FileAutoDownloadComponent } from './file-auto-download/file-auto-download.component'; + +const BYTES_TO_MB_CONVERSION_VALUE = 1048576; @Component({ selector: 'adf-document-list', @@ -367,7 +371,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte private alfrescoApiService: AlfrescoApiService, private nodeService: NodesApiService, private dataTableService: DataTableService, - private lockService: LockService) { + private lockService: LockService, + private dialog: MatDialog) { this.nodeService.nodeUpdated.subscribe((node) => { this.dataTableService.rowUpdate.next({id: node.id, obj: {entry: node}}); @@ -758,7 +763,16 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte onPreviewFile(node: NodeEntry) { if (node) { - this.preview.emit(new NodeEntityEvent(node)); + const sizeInMB = node.entry?.content?.sizeInBytes / BYTES_TO_MB_CONVERSION_VALUE; + + const fileAutoDownloadFlag: boolean = this.appConfig.get('viewer.enableFileAutoDownload', true); + const sizeThreshold: number = this.appConfig.get('viewer.fileAutoDownloadSizeThresholdInMB', 15); + + if (fileAutoDownloadFlag && sizeInMB && sizeInMB > sizeThreshold) { + this.dialog.open(FileAutoDownloadComponent, { disableClose: true, data: node }); + } else { + this.preview.emit(new NodeEntityEvent(node)); + } } } diff --git a/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.html b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.html new file mode 100644 index 0000000000..ad1aaf6f21 --- /dev/null +++ b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.html @@ -0,0 +1,24 @@ +
+

{{ 'ADF-DOCUMENT-LIST.FILE_AUTO_DOWNLOAD_DIALOG.HEADER' | translate }}

+
+ + {{ 'ADF-DOCUMENT-LIST.FILE_AUTO_DOWNLOAD_DIALOG.LABEL' | translate }} + + + + + diff --git a/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.spec.ts b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.spec.ts new file mode 100644 index 0000000000..d7cdbc5e38 --- /dev/null +++ b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.spec.ts @@ -0,0 +1,76 @@ +/*! + * @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 { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileAutoDownloadComponent } from './file-auto-download.component'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { By } from '@angular/platform-browser'; +import { CoreTestingModule } from '@alfresco/adf-core'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +const mockDialog = { + close: jasmine.createSpy('close') +}; +describe('FileAutoDownloadComponent', () => { + let matDialogRef: MatDialogRef; + let fixture: ComponentFixture; + + const getButton = (buttonId: string) => { + return fixture.debugElement.query(By.css(buttonId)).nativeElement; + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [FileAutoDownloadComponent], + imports: [ + TranslateModule.forRoot(), + CoreTestingModule + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: MatDialogRef, useValue: mockDialog }, + { provide: MAT_DIALOG_DATA, useValue: null } + ] + }); + + fixture = TestBed.createComponent(FileAutoDownloadComponent); + matDialogRef = TestBed.inject(MatDialogRef); + fixture.detectChanges(); + }); + + it('should emit FileAutoDownloadActionsEnum.CANCEL and close dialog when clicking on the cancel button', async () => { + const waitButton = getButton('#cancelButton'); + waitButton.dispatchEvent(new Event('click')); + + await fixture.detectChanges(); + await fixture.whenStable(); + + expect(matDialogRef.close).toHaveBeenCalled(); + }); + + it('should emit FileAutoDownloadActionsEnum.DOWNLOAD and close dialog when clicking on the wait button', async () => { + const waitButton = getButton('#downloadButton'); + waitButton.dispatchEvent(new Event('click')); + + await fixture.detectChanges(); + await fixture.whenStable(); + + expect(matDialogRef.close).toHaveBeenCalled(); + }); +}); diff --git a/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.ts b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.ts new file mode 100644 index 0000000000..09ad0ae64f --- /dev/null +++ b/lib/content-services/src/lib/document-list/components/file-auto-download/file-auto-download.component.ts @@ -0,0 +1,28 @@ +/*! + * @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, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { NodeEntry } from '@alfresco/js-api'; + +@Component({ + selector: 'adf-file-auto-download', + templateUrl: './file-auto-download.component.html' +}) +export class FileAutoDownloadComponent { + constructor(@Inject(MAT_DIALOG_DATA) public node: NodeEntry) {} +} diff --git a/lib/content-services/src/lib/document-list/document-list.module.ts b/lib/content-services/src/lib/document-list/document-list.module.ts index c0d9fbb17a..4db8c771dd 100644 --- a/lib/content-services/src/lib/document-list/document-list.module.ts +++ b/lib/content-services/src/lib/document-list/document-list.module.ts @@ -35,6 +35,8 @@ import { LibraryRoleColumnComponent } from './components/library-role-column/lib import { LibraryNameColumnComponent } from './components/library-name-column/library-name-column.component'; import { NameColumnComponent } from './components/name-column/name-column.component'; import { FilterHeaderComponent } from './components/filter-header/filter-header.component'; +import { FileAutoDownloadComponent } from './components/file-auto-download/file-auto-download.component'; +import { ContentDirectiveModule } from '../directives/content-directive.module'; @NgModule({ imports: [ @@ -45,7 +47,8 @@ import { FilterHeaderComponent } from './components/filter-header/filter-header. MaterialModule, UploadModule, EditJsonDialogModule, - SearchModule + SearchModule, + ContentDirectiveModule ], declarations: [ DocumentListComponent, @@ -56,7 +59,8 @@ import { FilterHeaderComponent } from './components/filter-header/filter-header. NameColumnComponent, ContentActionComponent, ContentActionListComponent, - FilterHeaderComponent + FilterHeaderComponent, + FileAutoDownloadComponent ], exports: [ DocumentListComponent, diff --git a/lib/content-services/src/lib/document-list/public-api.ts b/lib/content-services/src/lib/document-list/public-api.ts index d516b99a3a..6b9b77313a 100644 --- a/lib/content-services/src/lib/document-list/public-api.ts +++ b/lib/content-services/src/lib/document-list/public-api.ts @@ -25,6 +25,7 @@ export * from './components/library-status-column/library-status-column.componen export * from './components/name-column/name-column.component'; export * from './components/filter-header/filter-header.component'; export * from './components/trashcan-name-column/trashcan-name-column.component'; +export * from './components/file-auto-download/file-auto-download.component'; // data export * from './data/share-datatable-adapter'; diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index e94428de0b..4e297df3cb 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -73,7 +73,15 @@ "REMOVE": "Remove", "DOWNLOAD": "Download" }, - "LOADER_LABEL": "Document list loader" + "LOADER_LABEL": "Document list loader", + "FILE_AUTO_DOWNLOAD_DIALOG": { + "HEADER": "Preview loading unresponsive", + "LABEL": "Download to view this document, or cancel to continue without preview.", + "ACTIONS": { + "CANCEL": "Cancel", + "DOWNLOAD": "Download" + } + } }, "ALFRESCO_DOCUMENT_LIST": { "BUTTON": {