From cc5ac7525cfb64104a824d1bd23c7911aa998ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Chaco=CC=81n?= Date: Thu, 30 Jan 2025 17:38:22 -0600 Subject: [PATCH 1/2] added main link method on item component to use in thumbnails. --- .../journal/journal.component.spec.ts | 3 + .../publication/publication.component.html | 2 +- .../publication/publication.component.spec.ts | 3 + .../item-types/shared/item.component.spec.ts | 103 +++++++++++++++++- .../item-types/shared/item.component.ts | 38 ++++++- .../untyped-item/untyped-item.component.html | 2 +- .../untyped-item.component.spec.ts | 3 + .../thumbnail/themed-thumbnail.component.ts | 3 + src/app/thumbnail/thumbnail.component.html | 6 +- src/app/thumbnail/thumbnail.component.ts | 5 + 10 files changed, 161 insertions(+), 7 deletions(-) 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..6f7ec5113a2 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..8b411cb3ffc 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,9 +4,11 @@ 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'; @@ -20,6 +22,9 @@ import { isIiifEnabled, isIiifSearchEnabled, } from './item-iiif-utils'; +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'; @Component({ selector: 'ds-item', @@ -75,8 +80,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 +102,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 +114,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 => { + 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. From 216fee093f7e4117174b32dd9abc4f5080162efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Chaco=CC=81n?= Date: Tue, 4 Feb 2025 16:11:31 -0600 Subject: [PATCH 2/2] fix lint errors --- .../item-types/shared/item.component.spec.ts | 8 ++++---- .../simple/item-types/shared/item.component.ts | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) 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 6f7ec5113a2..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 @@ -587,7 +587,7 @@ describe('ItemComponent', () => { beforeEach(() => { bitstreamDataService = jasmine.createSpyObj('BitstreamDataService', [ 'findPrimaryBitstreamByItemAndName', - 'findAllByItemAndBundleName' + 'findAllByItemAndBundleName', ]); router = jasmine.createSpyObj('Router', ['navigate']); routeService = jasmine.createSpyObj('RouteService', ['getRoute']); @@ -603,7 +603,7 @@ describe('ItemComponent', () => { bundle: { href: 'bundle-link' }, format: { href: 'format-link' }, content: { href: 'primary-link' }, - thumbnail: { href: 'thumbnail-link' } + thumbnail: { href: 'thumbnail-link' }, }; const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([primaryBitstream])); bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(primaryBitstream)); @@ -622,7 +622,7 @@ describe('ItemComponent', () => { bundle: { href: 'bundle-link' }, format: { href: 'format-link' }, content: { href: 'primary-link' }, - thumbnail: { href: 'thumbnail-link' } + thumbnail: { href: 'thumbnail-link' }, }; const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([primaryBitstream])); bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(null)); @@ -642,7 +642,7 @@ describe('ItemComponent', () => { bundle: { href: 'bundle-link' }, format: { href: 'format-link' }, content: { href: 'primary-link' }, - thumbnail: { href: 'thumbnail-link' } + thumbnail: { href: 'thumbnail-link' }, }; const remotePaginatedListBitstream = createSuccessfulRemoteDataObject$(createPaginatedList([])); bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(of(null)); 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 8b411cb3ffc..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,13 +4,19 @@ import { OnInit, } from '@angular/core'; import { Router } from '@angular/router'; -import { Observable, of } 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'; @@ -22,9 +28,6 @@ import { isIiifEnabled, isIiifSearchEnabled, } from './item-iiif-utils'; -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'; @Component({ selector: 'ds-item', @@ -131,13 +134,13 @@ export class ItemComponent implements OnInit { map((bitstreams) => { const bitstreamList = bitstreams.payload.page; return (bitstreamList && bitstreamList.length > 0) ? bitstreamList[0]._links.content.href : ''; - }) + }), ); }), - catchError(error => { + catchError((error: unknown) => { console.error('Error fetching thumbnail link:', error); return of(''); - }) + }), ); } }