Merge pull request #4249 from atmire/fix-embargoed-thumbnails_contribute-9.0

Show restricted thumbnails to users with access rights
This commit is contained in:
Tim Donohue
2025-05-08 15:12:36 -05:00
committed by GitHub
6 changed files with 82 additions and 49 deletions

View File

@@ -26,6 +26,12 @@ describe('Homepage', () => {
// Wait for homepage tag to appear // Wait for homepage tag to appear
cy.get('ds-home-page').should('be.visible'); cy.get('ds-home-page').should('be.visible');
// Wait for at least one loading component to show up
cy.get('ds-loading').should('exist');
// Wait until all loading components have disappeared
cy.get('ds-loading').should('not.exist');
// Analyze <ds-home-page> for accessibility issues // Analyze <ds-home-page> for accessibility issues
testA11y('ds-home-page'); testA11y('ds-home-page');
}); });

View File

@@ -5,7 +5,7 @@
[showMessage]="false" [showMessage]="false"
></ds-loading> ></ds-loading>
} }
@if (!isLoading) { @else {
<div class="media-viewer"> <div class="media-viewer">
@if (mediaList.length > 0) { @if (mediaList.length > 0) {
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo"> <ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
@@ -33,17 +33,9 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
} @else { } @else {
@if (mediaOptions.image && mediaOptions.video) { <ds-thumbnail
<ds-media-viewer-image [thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder" </ds-thumbnail>
[preview]="false"
></ds-media-viewer-image>
}
@if (!(mediaOptions.image && mediaOptions.video)) {
<ds-thumbnail
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
</ds-thumbnail>
}
} }
</div> </div>
} }

View File

@@ -1,4 +1,7 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import {
NO_ERRORS_SCHEMA,
PLATFORM_ID,
} from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@@ -15,7 +18,9 @@ import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { FileService } from '../../core/shared/file.service';
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock'; import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
@@ -33,6 +38,9 @@ import { MediaViewerComponent } from './media-viewer.component';
describe('MediaViewerComponent', () => { describe('MediaViewerComponent', () => {
let comp: MediaViewerComponent; let comp: MediaViewerComponent;
let fixture: ComponentFixture<MediaViewerComponent>; let fixture: ComponentFixture<MediaViewerComponent>;
let authService;
let authorizationService;
let fileService;
const mockBitstream: Bitstream = Object.assign(new Bitstream(), { const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201, sizeBytes: 10201,
@@ -57,7 +65,7 @@ describe('MediaViewerComponent', () => {
'dc.title': [ 'dc.title': [
{ {
language: null, language: null,
value: 'test_word.docx', value: 'test_image.jpg',
}, },
], ],
}, },
@@ -75,6 +83,15 @@ describe('MediaViewerComponent', () => {
); );
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
authService = jasmine.createSpyObj('AuthService', {
isAuthenticated: observableOf(true),
});
authorizationService = jasmine.createSpyObj('AuthorizationService', {
isAuthorized: observableOf(true),
});
fileService = jasmine.createSpyObj('FileService', {
retrieveFileDownloadLink: null,
});
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot({ TranslateModule.forRoot({
@@ -90,6 +107,10 @@ describe('MediaViewerComponent', () => {
MetadataFieldWrapperComponent, MetadataFieldWrapperComponent,
], ],
providers: [ providers: [
{ provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: FileService, useValue: fileService },
{ provide: PLATFORM_ID, useValue: 'browser' },
{ provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: ThemeService, useValue: getMockThemeService() }, { provide: ThemeService, useValue: getMockThemeService() },
{ provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthService, useValue: new AuthServiceMock() },
@@ -153,9 +174,9 @@ describe('MediaViewerComponent', () => {
expect(mediaItem.thumbnail).toBe(null); expect(mediaItem.thumbnail).toBe(null);
}); });
it('should display a default, thumbnail', () => { it('should display a default thumbnail', () => {
const defaultThumbnail = fixture.debugElement.query( const defaultThumbnail = fixture.debugElement.query(
By.css('ds-media-viewer-image'), By.css('ds-thumbnail'),
); );
expect(defaultThumbnail.nativeElement).toBeDefined(); expect(defaultThumbnail.nativeElement).toBeDefined();
}); });

View File

@@ -1,5 +1,5 @@
<div class="thumbnail" [class.limit-width]="limitWidth"> <div class="thumbnail" [class.limit-width]="limitWidth">
@if (isLoading) { @if (isLoading()) {
<div class="thumbnail-content outer"> <div class="thumbnail-content outer">
<div class="inner"> <div class="inner">
<div class="centered"> <div class="centered">
@@ -9,16 +9,16 @@
</div> </div>
} }
<!-- don't use *ngIf="!isLoading" so the thumbnail can load in while the animation is playing --> <!-- don't use *ngIf="!isLoading" so the thumbnail can load in while the animation is playing -->
@if (src !== null) { @if (src() !== null) {
<img <img
class="thumbnail-content img-fluid" class="thumbnail-content img-fluid"
[ngClass]="{ 'd-none': isLoading }" [ngClass]="{ 'd-none': isLoading ()}"
[src]="src | dsSafeUrl" [src]="src() | dsSafeUrl"
[alt]="alt | translate" [alt]="alt | translate"
(error)="errorHandler()" (error)="errorHandler()"
(load)="successHandler()" (load)="successHandler()"
/> />
} @if (src === null && !isLoading) { } @if (src() === null && isLoading() === false) {
<div class="thumbnail-content outer"> <div class="thumbnail-content outer">
<div class="inner"> <div class="inner">
<div class="thumbnail-placeholder centered"> <div class="thumbnail-placeholder centered">

View File

@@ -99,32 +99,32 @@ describe('ThumbnailComponent', () => {
}); });
describe('loading', () => { describe('loading', () => {
it('should start out with isLoading$ true', () => { it('should start out with isLoading true', () => {
expect(comp.isLoading).toBeTrue(); expect(comp.isLoading()).toBeTrue();
}); });
it('should set isLoading$ to false once an image is successfully loaded', () => { it('should set isLoading$ to false once an image is successfully loaded', () => {
comp.setSrc('http://bit.stream'); comp.setSrc('http://bit.stream');
fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load')); fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load'));
expect(comp.isLoading).toBeFalse(); expect(comp.isLoading()).toBeFalse();
}); });
it('should set isLoading$ to false once the src is set to null', () => { it('should set isLoading$ to false once the src is set to null', () => {
comp.setSrc(null); comp.setSrc(null);
expect(comp.isLoading).toBeFalse(); expect(comp.isLoading()).toBeFalse();
}); });
it('should show a loading animation while isLoading$ is true', () => { it('should show a loading animation while isLoading$ is true', () => {
expect(de.query(By.css('ds-loading'))).toBeTruthy(); expect(de.query(By.css('ds-loading'))).toBeTruthy();
comp.isLoading = false; comp.isLoading.set(false);
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-loading'))).toBeFalsy(); expect(fixture.debugElement.query(By.css('ds-loading'))).toBeFalsy();
}); });
describe('with a thumbnail image', () => { describe('with a thumbnail image', () => {
beforeEach(() => { beforeEach(() => {
comp.src = 'https://bit.stream'; comp.src.set('https://bit.stream');
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -133,7 +133,7 @@ describe('ThumbnailComponent', () => {
expect(img).toBeTruthy(); expect(img).toBeTruthy();
expect(img.classes['d-none']).toBeTrue(); expect(img.classes['d-none']).toBeTrue();
comp.isLoading = false; comp.isLoading.set(false);
fixture.detectChanges(); fixture.detectChanges();
img = fixture.debugElement.query(By.css('img.thumbnail-content')); img = fixture.debugElement.query(By.css('img.thumbnail-content'));
expect(img).toBeTruthy(); expect(img).toBeTruthy();
@@ -144,14 +144,14 @@ describe('ThumbnailComponent', () => {
describe('without a thumbnail image', () => { describe('without a thumbnail image', () => {
beforeEach(() => { beforeEach(() => {
comp.src = null; comp.src.set(null);
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should only show the HTML placeholder once done loading', () => { it('should only show the HTML placeholder once done loading', () => {
expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy(); expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy();
comp.isLoading = false; comp.isLoading.set(false);
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy();
}); });
@@ -247,14 +247,14 @@ describe('ThumbnailComponent', () => {
describe('fallback', () => { describe('fallback', () => {
describe('if there is a default image', () => { describe('if there is a default image', () => {
it('should display the default image', () => { it('should display the default image', () => {
comp.src = 'http://bit.stream'; comp.src.set('http://bit.stream');
comp.defaultImage = 'http://default.img'; comp.defaultImage = 'http://default.img';
comp.errorHandler(); comp.errorHandler();
expect(comp.src).toBe(comp.defaultImage); expect(comp.src()).toBe(comp.defaultImage);
}); });
it('should include the alt text', () => { it('should include the alt text', () => {
comp.src = 'http://bit.stream'; comp.src.set('http://bit.stream');
comp.defaultImage = 'http://default.img'; comp.defaultImage = 'http://default.img';
comp.errorHandler(); comp.errorHandler();
@@ -266,10 +266,10 @@ describe('ThumbnailComponent', () => {
describe('if there is no default image', () => { describe('if there is no default image', () => {
it('should display the HTML placeholder', () => { it('should display the HTML placeholder', () => {
comp.src = 'http://default.img'; comp.src.set('http://default.img');
comp.defaultImage = null; comp.defaultImage = null;
comp.errorHandler(); comp.errorHandler();
expect(comp.src).toBe(null); expect(comp.src()).toBe(null);
fixture.detectChanges(); fixture.detectChanges();
const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement;
@@ -363,7 +363,7 @@ describe('ThumbnailComponent', () => {
it('should show the default image', () => { it('should show the default image', () => {
comp.defaultImage = 'default/image.jpg'; comp.defaultImage = 'default/image.jpg';
comp.ngOnChanges({}); comp.ngOnChanges({});
expect(comp.src).toBe('default/image.jpg'); expect(comp.src()).toBe('default/image.jpg');
}); });
}); });
}); });
@@ -419,7 +419,7 @@ describe('ThumbnailComponent', () => {
}); });
it('should start out with isLoading$ true', () => { it('should start out with isLoading$ true', () => {
expect(comp.isLoading).toBeTrue(); expect(comp.isLoading()).toBeTrue();
expect(de.query(By.css('ds-loading'))).toBeTruthy(); expect(de.query(By.css('ds-loading'))).toBeTruthy();
}); });

View File

@@ -8,7 +8,9 @@ import {
Input, Input,
OnChanges, OnChanges,
PLATFORM_ID, PLATFORM_ID,
signal,
SimpleChanges, SimpleChanges,
WritableSignal,
} from '@angular/core'; } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@@ -26,7 +28,6 @@ import {
} from '../shared/empty.util'; } from '../shared/empty.util';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe';
import { VarDirective } from '../shared/utils/var.directive';
/** /**
* This component renders a given Bitstream as a thumbnail. * This component renders a given Bitstream as a thumbnail.
@@ -38,7 +39,7 @@ import { VarDirective } from '../shared/utils/var.directive';
styleUrls: ['./thumbnail.component.scss'], styleUrls: ['./thumbnail.component.scss'],
templateUrl: './thumbnail.component.html', templateUrl: './thumbnail.component.html',
standalone: true, standalone: true,
imports: [VarDirective, CommonModule, ThemedLoadingComponent, TranslateModule, SafeUrlPipe], imports: [CommonModule, ThemedLoadingComponent, TranslateModule, SafeUrlPipe],
}) })
export class ThumbnailComponent implements OnChanges { export class ThumbnailComponent implements OnChanges {
/** /**
@@ -55,7 +56,7 @@ export class ThumbnailComponent implements OnChanges {
/** /**
* The src attribute used in the template to render the image. * The src attribute used in the template to render the image.
*/ */
src: string = undefined; src: WritableSignal<string> = signal(undefined);
retriedWithToken = false; retriedWithToken = false;
@@ -78,7 +79,7 @@ export class ThumbnailComponent implements OnChanges {
* Whether the thumbnail is currently loading * Whether the thumbnail is currently loading
* Start out as true to avoid flashing the alt text while a thumbnail is being loaded. * Start out as true to avoid flashing the alt text while a thumbnail is being loaded.
*/ */
isLoading = true; isLoading: WritableSignal<boolean> = signal(true);
constructor( constructor(
@Inject(PLATFORM_ID) private platformID: any, @Inject(PLATFORM_ID) private platformID: any,
@@ -134,7 +135,7 @@ export class ThumbnailComponent implements OnChanges {
* Otherwise, fall back to the default image or a HTML placeholder * Otherwise, fall back to the default image or a HTML placeholder
*/ */
errorHandler() { errorHandler() {
const src = this.src; const src = this.src();
const thumbnail = this.bitstream; const thumbnail = this.bitstream;
const thumbnailSrc = thumbnail?._links?.content?.href; const thumbnailSrc = thumbnail?._links?.content?.href;
@@ -186,9 +187,22 @@ export class ThumbnailComponent implements OnChanges {
* @param src * @param src
*/ */
setSrc(src: string): void { setSrc(src: string): void {
this.src = src; // only update the src if it has changed (the parent component may fire the same one multiple times
if (src === null) { if (this.src() !== src) {
this.isLoading = false; // every time the src changes we need to start the loading animation again, as it's possible
// that it is first set to null when the parent component initializes and then set to
// the actual value
//
// isLoading$ will be set to false by the error or success handler afterwards, except in the
// case where src is null, then we have to set it manually here (because those handlers won't
// trigger)
if (src !== null && this.isLoading() === false) {
this.isLoading.set(true);
}
this.src.set(src);
if (src === null && this.isLoading() === true) {
this.isLoading.set(false);
}
} }
} }
@@ -196,6 +210,6 @@ export class ThumbnailComponent implements OnChanges {
* Stop the loading animation once the thumbnail is successfully loaded * Stop the loading animation once the thumbnail is successfully loaded
*/ */
successHandler() { successHandler() {
this.isLoading = false; this.isLoading.set(false);
} }
} }