[ACS-5184] Show progress spinner on file navigation (#10596)

* [ACS-5184] show progress spinner on file navigation

* [ACS-5184] show spinner unitl content is ready

* [ACS-5184] unit test fix

* [ACS-5184] cr fix

* [ACS-5184] spelling error fix
This commit is contained in:
Mykyta Maliarchuk 2025-04-07 14:13:34 +02:00 committed by GitHub
parent 2d21340947
commit cd63f67e0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 140 additions and 22 deletions

View File

@ -8,6 +8,7 @@
<img #image id="viewer-image" <img #image id="viewer-image"
[src]="urlFile" [src]="urlFile"
[alt]="fileName" [alt]="fileName"
(load)="imageLoaded.emit()"
(error)="onImageError()" /> (error)="onImageError()" />
</div> </div>

View File

@ -80,6 +80,9 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
@Output() @Output()
isSaving = new EventEmitter<boolean>(); isSaving = new EventEmitter<boolean>();
@Output()
imageLoaded = new EventEmitter<void>();
@ViewChild('image', { static: false }) @ViewChild('image', { static: false })
imageElement: ElementRef; imageElement: ElementRef;

View File

@ -1,4 +1,4 @@
<video controls class="adf-video-player" <video controls class="adf-video-player" (canplay)="canPlay.emit()"
[ngClass]="{ 'adf-audio-file': mimeType && mimeType.startsWith('audio') }"> [ngClass]="{ 'adf-audio-file': mimeType && mimeType.startsWith('audio') }">
<source [src]="urlFile" <source [src]="urlFile"
[type]="mimeType" [type]="mimeType"

View File

@ -49,6 +49,9 @@ export class MediaPlayerComponent implements OnChanges {
@Output() @Output()
error = new EventEmitter<any>(); error = new EventEmitter<any>();
@Output()
canPlay = new EventEmitter<void>();
constructor(private urlService: UrlService) {} constructor(private urlService: UrlService) {}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {

View File

@ -103,6 +103,9 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
@Output() @Output()
close = new EventEmitter<any>(); close = new EventEmitter<any>();
@Output()
pagesLoaded = new EventEmitter<void>();
page: number; page: number;
displayPage: number; displayPage: number;
totalPages: number; totalPages: number;
@ -565,6 +568,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
* *
*/ */
onPagesLoaded() { onPagesLoaded() {
this.pagesLoaded.emit();
this.isPanelDisabled = false; this.isPanelDisabled = false;
} }

View File

@ -1,16 +1,13 @@
<div *ngIf="isLoading" <div *ngIf="(viewerType === 'media' || viewerType === 'pdf' || viewerType === 'image') ? isLoading || !isContentReady : isLoading"
class="adf-viewer-render-main"> class="adf-viewer-render-main-loader">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container"> <div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container"> <div class="adf-viewer-render-content-container">
<ng-container *ngIf="isLoading"> <div class="adf-viewer-render__loading-screen ">
<div class="adf-viewer-render__loading-screen"> <h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2> <div>
<div> <mat-spinner class="adf-viewer-render__loading-screen__spinner"/>
<mat-spinner class="adf-viewer-render__loading-screen__spinner" />
</div>
</div> </div>
</ng-container> </div>
</div> </div>
</div> </div>
</div> </div>
@ -35,6 +32,7 @@
[urlFile]="urlFile" [urlFile]="urlFile"
[fileName]="internalFileName" [fileName]="internalFileName"
[cacheType]="cacheTypeForContent" [cacheType]="cacheTypeForContent"
(pagesLoaded)="isContentReady = true"
(close)="onClose()" (close)="onClose()"
(error)="onUnsupportedFile()" /> (error)="onUnsupportedFile()" />
</ng-container> </ng-container>
@ -47,6 +45,7 @@
[blobFile]="blobFile" [blobFile]="blobFile"
(error)="onUnsupportedFile()" (error)="onUnsupportedFile()"
(submit)="onSubmitFile($event)" (submit)="onSubmitFile($event)"
(imageLoaded)="isContentReady = true"
(isSaving)="isSaving.emit($event)" (isSaving)="isSaving.emit($event)"
/> />
</ng-container> </ng-container>
@ -58,7 +57,8 @@
[mimeType]="mimeType" [mimeType]="mimeType"
[blobFile]="blobFile" [blobFile]="blobFile"
[fileName]="internalFileName" [fileName]="internalFileName"
(error)="onUnsupportedFile()" /> (error)="onUnsupportedFile()"
(canPlay)="isContentReady = true"/>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'text'"> <ng-container *ngSwitchCase="'text'">

View File

@ -7,6 +7,18 @@
background-color: var(--adf-theme-background-card-color); background-color: var(--adf-theme-background-card-color);
} }
.adf-viewer-render-main-loader {
position: fixed;
top: 64px;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.adf-viewer-render { .adf-viewer-render {
&-main { &-main {
width: 0; width: 0;

View File

@ -18,13 +18,13 @@
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions'; import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing'; import { SpyLocation } from '@angular/common/testing';
import { Component, TemplateRef, ViewChild } from '@angular/core'; import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { NoopTranslateModule, UnitTestingUtils } from '../../../testing'; import { NoopTranslateModule, UnitTestingUtils } from '../../../testing';
import { RenderingQueueServices } from '../../services/rendering-queue.services'; import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { ViewerRenderComponent } from './viewer-render.component'; import { ViewerRenderComponent } from './viewer-render.component';
import { ViewerExtensionDirective } from '@alfresco/adf-core'; import { ImgViewerComponent, MediaPlayerComponent, PdfViewerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({ @Component({
@ -476,4 +476,75 @@ describe('ViewerComponent', () => {
}); });
}); });
}); });
describe('Spinner', () => {
const getMainLoader = (): DebugElement => testingUtils.getByCSS('.adf-viewer-render-main-loader');
it('should show spinner when isLoading is true', () => {
component.isLoading = true;
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
});
it('should show spinner until content is ready when viewerType is media', () => {
component.isLoading = false;
component.urlFile = 'some-file.mp4';
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
const mediaViewer = testingUtils.getByDirective(MediaPlayerComponent);
mediaViewer.triggerEventHandler('canPlay', null);
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.viewerType).toBe('media');
});
it('should show spinner until content is ready when viewerType is pdf', () => {
component.isLoading = false;
component.urlFile = 'some-url.pdf';
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
const pdfViewer = testingUtils.getByDirective(PdfViewerComponent);
pdfViewer.triggerEventHandler('pagesLoaded', null);
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.viewerType).toBe('pdf');
});
it('should show spinner until content is ready when viewerType is image', () => {
component.isLoading = false;
component.urlFile = 'some-url.png';
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
const imgViewer = testingUtils.getByDirective(ImgViewerComponent);
imgViewer.triggerEventHandler('imageLoaded', null);
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.viewerType).toBe('image');
});
it('should not show spinner when isLoading = false and isContentReady = false for other viewer types', () => {
component.isLoading = false;
component.urlFile = 'some-url.txt';
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.isContentReady).toBeFalse();
});
});
}); });

View File

@ -142,6 +142,7 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
extension: string; extension: string;
internalFileName: string; internalFileName: string;
viewerType: string = 'unknown'; viewerType: string = 'unknown';
isContentReady = false;
/** /**
* Returns a list of the active Viewer content extensions. * Returns a list of the active Viewer content extensions.
@ -184,6 +185,7 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
} }
ngOnChanges() { ngOnChanges() {
this.isContentReady = false;
this.isLoading = !this.blobFile && !this.urlFile; this.isLoading = !this.blobFile && !this.urlFile;
if (this.blobFile) { if (this.blobFile) {

View File

@ -121,6 +121,26 @@ describe('ViewerComponent', () => {
expect(thumbnailService.getMimeTypeIcon).toHaveBeenCalledWith('application/pdf'); expect(thumbnailService.getMimeTypeIcon).toHaveBeenCalledWith('application/pdf');
expect(component.mimeTypeIconUrl).toBe('application/pdf'); expect(component.mimeTypeIconUrl).toBe('application/pdf');
}); });
it('should reset urlFile and blobFile on onNavigateBeforeClick', () => {
component.urlFile = 'some-url';
component.blobFile = new Blob(['content'], { type: 'text/plain' });
component.onNavigateBeforeClick(new MouseEvent('click'));
expect(component.urlFile).toBe('');
expect(component.blobFile).toBeNull();
});
it('should reset urlFile and blobFile on onNavigateNextClick', () => {
component.urlFile = 'some-url';
component.blobFile = new Blob(['content'], { type: 'text/plain' });
component.onNavigateNextClick(new MouseEvent('click'));
expect(component.urlFile).toBe('');
expect(component.blobFile).toBeNull();
});
}); });
describe('File Name Test', () => { describe('File Name Test', () => {

View File

@ -387,10 +387,12 @@ export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
} }
onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) { onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) {
this.resetLoadingSpinner();
this.navigateBefore.next(event); this.navigateBefore.next(event);
} }
onNavigateNextClick(event: MouseEvent | KeyboardEvent) { onNavigateNextClick(event: MouseEvent | KeyboardEvent) {
this.resetLoadingSpinner();
this.navigateNext.next(event); this.navigateNext.next(event);
} }
@ -416,22 +418,17 @@ export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
return; return;
} }
const key = event.keyCode; if (event.key === 'ArrowLeft' && this.canNavigateBefore) {
// Left arrow
if (key === 37 && this.canNavigateBefore) {
event.preventDefault(); event.preventDefault();
this.onNavigateBeforeClick(event); this.onNavigateBeforeClick(event);
} }
// Right arrow if (event.key === 'ArrowRight' && this.canNavigateNext) {
if (key === 39 && this.canNavigateNext) {
event.preventDefault(); event.preventDefault();
this.onNavigateNextClick(event); this.onNavigateNextClick(event);
} }
// Ctrl+F if (event.code === 'KeyF' && event.ctrlKey) {
if (key === 70 && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
this.enterFullScreen(); this.enterFullScreen();
} }
@ -527,4 +524,9 @@ export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
}); });
} }
} }
private resetLoadingSpinner() {
this.urlFile = '';
this.blobFile = null;
}
} }