diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 1ae584455a2..daa6c143e3b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -96,6 +96,9 @@ describe('JournalComponent', () => { getThumbnailFor(item: Item): Observable> { return createSuccessfulRemoteDataObject$(new Bitstream()); }, + findPrimaryBitstreamByItemAndName(): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 86f203deca2..b86939da235 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -17,7 +17,7 @@
- +
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index 49c486b16b2..ab7f622b896 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -97,6 +97,9 @@ describe('PublicationComponent', () => { getThumbnailFor(item: Item): Observable> { return createSuccessfulRemoteDataObject$(new Bitstream()); }, + findPrimaryBitstreamByItemAndName(): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, }; TestBed.configureTestingModule({ imports: [ diff --git a/src/app/item-page/simple/item-types/shared/item.component.spec.ts b/src/app/item-page/simple/item-types/shared/item.component.spec.ts index e1ae24ff4d2..5c23d728c12 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.spec.ts @@ -22,6 +22,8 @@ import { import { Observable, of as observableOf, + of, + throwError, } from 'rxjs'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; @@ -128,6 +130,9 @@ export function getItemPageFieldsTest(mockItem: Item, component) { getThumbnailFor(item: Item): Observable> { return createSuccessfulRemoteDataObject$(new Bitstream()); }, + findPrimaryBitstreamByItemAndName(): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, }; const authorizationService = jasmine.createSpyObj('authorizationService', { @@ -484,6 +489,15 @@ describe('ItemComponent', () => { const recentSubmissionsUrl = '/collections/be7b8430-77a5-4016-91c9-90863e50583a?cp.page=3'; beforeEach(waitForAsync(() => { + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, + findPrimaryBitstreamByItemAndName(): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, + }; + TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ @@ -511,7 +525,7 @@ describe('ItemComponent', () => { { provide: VersionDataService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, - { provide: BitstreamDataService, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, @@ -564,4 +578,91 @@ describe('ItemComponent', () => { }); }); + describe('when calling getThumbnailLink ', () => { + let component: ItemComponent; + let bitstreamDataService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let routeService: jasmine.SpyObj; + + beforeEach(() => { + bitstreamDataService = jasmine.createSpyObj('BitstreamDataService', [ + 'findPrimaryBitstreamByItemAndName', + 'findAllByItemAndBundleName', + ]); + router = jasmine.createSpyObj('Router', ['navigate']); + routeService = jasmine.createSpyObj('RouteService', ['getRoute']); + + component = new ItemComponent(routeService, router, bitstreamDataService); + }); + + it('should return the primary bitstream link if available', (done) => { + const item = new Item(); + const primaryBitstream = new Bitstream(); + primaryBitstream._links = { + self: { href: 'self-link' }, + bundle: { href: 'bundle-link' }, + format: { href: 'format-link' }, + content: { href: 'primary-link' }, + thumbnail: { href: 'thumbnail-link' }, + }; + const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([primaryBitstream])); + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(primaryBitstream)); + + component.getThumbnailLink(item).subscribe(link => { + expect(link).toBe('primary-link'); + done(); + }); + }); + + it('should return the first bitstream link if no primary bitstream is available', (done) => { + const item = new Item(); + const primaryBitstream = new Bitstream(); + primaryBitstream._links = { + self: { href: 'self-link' }, + bundle: { href: 'bundle-link' }, + format: { href: 'format-link' }, + content: { href: 'primary-link' }, + thumbnail: { href: 'thumbnail-link' }, + }; + const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([primaryBitstream])); + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(null)); + bitstreamDataService.findAllByItemAndBundleName.and.returnValue(remotePaginatedListBitstream); + + component.getThumbnailLink(item).subscribe(link => { + expect(link).toBe('primary-link'); + done(); + }); + }); + + it('should return an empty string if no bitstream is available', (done) => { + const item = new Item(); + const primaryBitstream = new Bitstream(); + primaryBitstream._links = { + self: { href: 'self-link' }, + bundle: { href: 'bundle-link' }, + format: { href: 'format-link' }, + content: { href: 'primary-link' }, + thumbnail: { href: 'thumbnail-link' }, + }; + const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([])); + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(null)); + bitstreamDataService.findAllByItemAndBundleName.and.returnValue(remotePaginatedListBitstream); + + component.getThumbnailLink(item).subscribe(link => { + expect(link).toBe(''); + done(); + }); + }); + + it('should return an empty string if an error occurs', (done) => { + const item = new Item(); + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(throwError(() => new Error('Network error'))); + + component.getThumbnailLink(item).subscribe(link => { + expect(link).toBe(''); + done(); + }); + }); + }); + }); diff --git a/src/app/item-page/simple/item-types/shared/item.component.ts b/src/app/item-page/simple/item-types/shared/item.component.ts index b93b7215c59..96b8b13b06a 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.ts @@ -4,11 +4,19 @@ import { OnInit, } from '@angular/core'; import { Router } from '@angular/router'; -import { Observable } from 'rxjs'; import { + Observable, + of, +} from 'rxjs'; +import { + catchError, map, + switchMap, take, } from 'rxjs/operators'; +import { BitstreamDataService } from 'src/app/core/data/bitstream-data.service'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; import { environment } from '../../../../../environments/environment'; import { RouteService } from '../../../../core/services/route.service'; @@ -75,8 +83,11 @@ export class ItemComponent implements OnInit { mediaViewer; + thumbnailLink$: Observable; + constructor(protected routeService: RouteService, - protected router: Router) { + protected router: Router, + private bitstreamDataService: BitstreamDataService) { this.mediaViewer = environment.mediaViewer; } @@ -94,7 +105,6 @@ export class ItemComponent implements OnInit { }; ngOnInit(): void { - this.itemPageRoute = getItemPageRoute(this.object); // hide/show the back button this.showBackButton$ = this.routeService.getPreviousUrl().pipe( @@ -107,5 +117,30 @@ export class ItemComponent implements OnInit { if (this.iiifSearchEnabled) { this.iiifQuery$ = getDSpaceQuery(this.object, this.routeService); } + this.thumbnailLink$ = this.getThumbnailLink(this.object); + } + + /** + * Get item's primary bitstream link or item's first bitstream link if there's no primary associated + */ + getThumbnailLink(item: Item): Observable { + return this.bitstreamDataService.findPrimaryBitstreamByItemAndName(item, 'ORIGINAL', true, true).pipe( + switchMap((primaryBitstream: Bitstream | null) => { + if (primaryBitstream) { + return of(primaryBitstream._links.content.href); + } + return this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL', {}, true, true).pipe( + getFirstCompletedRemoteData(), + map((bitstreams) => { + const bitstreamList = bitstreams.payload.page; + return (bitstreamList && bitstreamList.length > 0) ? bitstreamList[0]._links.content.href : ''; + }), + ); + }), + catchError((error: unknown) => { + console.error('Error fetching thumbnail link:', error); + return of(''); + }), + ); } } diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index 962cc2fcad8..5f255386aba 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -18,7 +18,7 @@
- +
diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index a03b76e24eb..713232d99a2 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -94,6 +94,9 @@ describe('UntypedItemComponent', () => { getThumbnailFor(item: Item): Observable> { return createSuccessfulRemoteDataObject$(new Bitstream()); }, + findPrimaryBitstreamByItemAndName(): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, }; TestBed.configureTestingModule({ imports: [ diff --git a/src/app/thumbnail/themed-thumbnail.component.ts b/src/app/thumbnail/themed-thumbnail.component.ts index 6d14378d3a5..52800669233 100644 --- a/src/app/thumbnail/themed-thumbnail.component.ts +++ b/src/app/thumbnail/themed-thumbnail.component.ts @@ -19,6 +19,8 @@ export class ThemedThumbnailComponent extends ThemedComponent; + @Input() link: string = undefined; + @Input() defaultImage?: string | null; @Input() alt?: string; @@ -29,6 +31,7 @@ export class ThemedThumbnailComponent extends ThemedComponent
- + + +
diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index cc583c3998f..823962a93ff 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -41,6 +41,11 @@ export class ThumbnailComponent implements OnChanges { */ @Input() thumbnail: Bitstream | RemoteData; + /** + * A link to open when a user clicks on thumbnail image + */ + @Input() link: string = undefined; + /** * The default image, used if the thumbnail isn't set or can't be downloaded. * If defaultImage is null, a HTML placeholder is used instead.