From 8007da9cbd152e2ee1db7fba00fb8ab597a68831 Mon Sep 17 00:00:00 2001 From: Marsa Haoua Date: Fri, 24 Apr 2026 16:41:48 +0200 Subject: [PATCH 1/2] Fix #12303: Evaluating IIIF metadata values dspace.iiif.enabled and iiif.search.enabled correctly --- src/app/core/utilities/item-iiif-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/utilities/item-iiif-utils.ts b/src/app/core/utilities/item-iiif-utils.ts index ec96fec3de2..6b684f0e07d 100644 --- a/src/app/core/utilities/item-iiif-utils.ts +++ b/src/app/core/utilities/item-iiif-utils.ts @@ -13,12 +13,12 @@ import { RouteService } from '../services/route.service'; import { Item } from '../shared/item.model'; export const isIiifEnabled = (item: Item) => { - return !!item.firstMetadataValue('dspace.iiif.enabled'); + return String(item.firstMetadataValue('dspace.iiif.enabled')?.valueOf?.() || '').trim().toLowerCase() === 'true'; }; export const isIiifSearchEnabled = (item: Item) => { - return !!item.firstMetadataValue('iiif.search.enabled'); + return String(item.firstMetadataValue('iiif.search.enabled')?.valueOf?.() || '').trim().toLowerCase() === 'true'; }; From f8e99d48627a831a9fa0decafed7670c6b2b8d81 Mon Sep 17 00:00:00 2001 From: Marsa Haoua Date: Fri, 15 May 2026 19:20:21 +0200 Subject: [PATCH 2/2] Make sure that the Mirador viewer is only embedded if IIIF is enabled for the entire repository. Relates to #12303. --- .../mirador-viewer.component.html | 14 +- .../mirador-viewer.component.spec.ts | 359 ++++++++++-------- .../mirador-viewer.component.ts | 9 + .../mirador-viewer/mirador-viewer.service.ts | 23 +- 4 files changed, 234 insertions(+), 171 deletions(-) diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.html b/src/app/item-page/mirador-viewer/mirador-viewer.component.html index 7f2d0c0aac3..93e8d1cd1ef 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.html +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.html @@ -1,8 +1,10 @@ -

{{'iiifviewer.fullscreen.notice' | translate}}

-@if (!isViewerAvailable) { -

{{viewerMessage}}

-} -@if (isViewerAvailable) { - +@if (isIiifEnabled$ | async) { +

{{'iiifviewer.fullscreen.notice' | translate}}

+ @if (!isViewerAvailable) { +

{{viewerMessage}}

+ } + @if (isViewerAvailable) { + + } } diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts index 39e515cc26f..a0bbd5942ab 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts @@ -4,8 +4,11 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { BitstreamDataService } from '@dspace/core/data/bitstream-data.service'; import { BundleDataService } from '@dspace/core/data/bundle-data.service'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; import { Item } from '@dspace/core/shared/item.model'; import { MetadataMap } from '@dspace/core/shared/metadata.models'; import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock'; @@ -16,6 +19,11 @@ import { TranslateModule, } from '@ngx-translate/core'; import { of } from 'rxjs'; +import { + skip, + take, + toArray, +} from 'rxjs/operators'; import { HostWindowService } from '../../shared/host-window.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; @@ -23,14 +31,6 @@ import { MiradorViewerComponent } from './mirador-viewer.component'; import { MiradorViewerService } from './mirador-viewer.service'; -function getItem(metadata: MetadataMap) { - return Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), - metadata: metadata, - relationships: createRelationshipsObservable(), - }); -} - const noMetadata = new MetadataMap(); const mockHostWindowService = { @@ -38,58 +38,100 @@ const mockHostWindowService = { widthCategory: of(true), }; -describe('MiradorViewerComponent with search', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); +let comp: MiradorViewerComponent; +let fixture: ComponentFixture; +let configurationDataService: jasmine.SpyObj; +let viewerService: jasmine.SpyObj; - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ +function getItem(metadata: MetadataMap) { + return Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: metadata, + relationships: createRelationshipsObservable(), + }); +} + +function setupTestBed(overrides?: { + showEmbedded?: boolean; + imageCount?: number; + iiifEnabled?: boolean; +}) { + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', [ + 'findByPropertyName', + ]); + + viewerService = jasmine.createSpyObj('MiradorViewerService', [ + 'showEmbeddedViewer', + 'getImageCount', + 'isIiifEnabled', + ]); + + viewerService.showEmbeddedViewer.and.returnValue(overrides?.showEmbedded ?? true); + viewerService.getImageCount.and.returnValue(of(overrides?.imageCount ?? 1)); + viewerService.isIiifEnabled.and.returnValue(of(overrides?.iiifEnabled ?? true)); + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + }), + MiradorViewerComponent, + ], + providers: [ + { provide: BitstreamDataService, useValue: {} }, + { provide: BundleDataService, useValue: {} }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: HostWindowService, useValue: mockHostWindowService }, + { provide: MiradorViewerService, useValue: viewerService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }); +} + +function createComponent(options?: { + searchable?: boolean; + object?: any; +}) { + fixture = TestBed.createComponent(MiradorViewerComponent); + comp = fixture.componentInstance; + comp.object = options?.object ?? getItem(noMetadata); + comp.searchable = options?.searchable ?? false; + fixture.detectChanges(); +} + +function getViewerSrc(): string { + return fixture.debugElement.query(By.css('#mirador-viewer')) + .nativeElement.src; +} + +describe('MiradorViewerComponent with search', () => { + beforeEach(waitForAsync(() => { + setupTestBed(); + TestBed.compileComponents(); })); describe('searchable item', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - comp.searchable = true; - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent({ searchable: true }); + }); it('should set multi property to true', (() => { expect(comp.multi).toBe(true); })); - it('should set url "multi" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('multi=true'); - })); + it('should set url "multi" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('multi=true'); + }); - it('should set url "searchable" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('searchable=true'); - })); + it('should set url "searchable" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('searchable=true'); + }); it('should not call mirador service image count', () => { expect(viewerService.getImageCount).not.toHaveBeenCalled(); @@ -99,108 +141,52 @@ describe('MiradorViewerComponent with search', () => { }); describe('MiradorViewerComponent with multiple images', () => { - - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - viewerService.getImageCount.and.returnValue(of(2)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + setupTestBed({ imageCount: 2 }); + TestBed.compileComponents(); })); describe('non-searchable item with multiple images', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - comp.searchable = false; - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should set url "multi" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('multi=true'); - })); + it('should set url "multi" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('multi=true'); + }); it('should call mirador service image count', () => { expect(viewerService.getImageCount).toHaveBeenCalled(); }); - it('should omit "searchable" param from url', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).not.toContain('searchable=true'); - })); + it('should omit "searchable" param from url', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).not.toContain('searchable=true'); + }); }); }); describe('MiradorViewerComponent with a single image', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - viewerService.getImageCount.and.returnValue(of(1)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + setupTestBed({ imageCount: 1 }); + TestBed.compileComponents(); })); describe('single image viewer', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should omit "multi" param', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).not.toContain('multi=false'); - })); + it('should omit "multi" param', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).not.toContain('multi=false'); + }); it('should call mirador service image count', () => { expect(viewerService.getImageCount).toHaveBeenCalled(); @@ -211,54 +197,99 @@ describe('MiradorViewerComponent with a single image', () => { }); describe('MiradorViewerComponent in development mode', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(false); - viewerService.getImageCount.and.returnValue(of(1)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - }, - }).compileComponents(); + setupTestBed({ + showEmbedded: false, + imageCount: 1, + }); + TestBed.compileComponents(); })); describe('embedded viewer', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should not embed the viewer', (() => { + it('should not embed the viewer', async () => { + await fixture.whenStable(); + fixture.detectChanges(); const value = fixture.debugElement .nativeElement.querySelector('#mirador-viewer'); expect(value).toBeNull(); - })); + }); - it('should show message', (() => { + it('should show message', async () => { + await fixture.whenStable(); + fixture.detectChanges(); const value = fixture.debugElement .nativeElement.querySelector('#viewer-message'); expect(value).not.toBeNull(); - })); + }); }); }); + + +describe('MiradorViewerService whether IIIF is enabled in the repository', () => { + let miradorViewerService: MiradorViewerService; + function mockIiifEnabled(values: string[]): void { + configurationDataService.findByPropertyName.and.returnValue( + createSuccessfulRemoteDataObject$( + Object.assign(new ConfigurationProperty(), { + name: 'iiif.enabled', + values, + }), + ), + ); + } + beforeEach(() => { + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', [ + 'findByPropertyName', + ]); + miradorViewerService = new MiradorViewerService(); + }); + describe('isIiifEnabled', () => { + it('should return false initially and then true', (done) => { + mockIiifEnabled(['true']); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(take(2), toArray()) + .subscribe((results) => { + expect(results).toEqual([false, true]); + done(); + }); + }); + + it('should return true when iiif.enabled is true', (done) => { + mockIiifEnabled(['true']); + + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeTrue(); + expect(configurationDataService.findByPropertyName) + .toHaveBeenCalledWith('iiif.enabled'); + done(); + }); + }); + + it('should return false when iiif.enabled is false', (done) => { + mockIiifEnabled(['false']); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeFalse(); + done(); + }); + }); + + it('should return false when configuration value is missing', (done) => { + mockIiifEnabled([]); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeFalse(); + done(); + }); + }); + }); +}); diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts index 04ff4a9fa88..7700d304482 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts @@ -16,6 +16,7 @@ import { } from '@angular/platform-browser'; import { BitstreamDataService } from '@dspace/core/data/bitstream-data.service'; import { BundleDataService } from '@dspace/core/data/bundle-data.service'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; import { WidthCategory } from '@dspace/core/shared/host-window-type'; import { Item } from '@dspace/core/shared/item.model'; import { TranslateModule } from '@ngx-translate/core'; @@ -61,6 +62,11 @@ export class MiradorViewerComponent implements OnInit { */ isViewerAvailable = true; + /** + * Check if IIIF is enabled in the repository. + */ + isIiifEnabled$: Observable; + /** * The url for the iframe. */ @@ -83,6 +89,7 @@ export class MiradorViewerComponent implements OnInit { private bitstreamDataService: BitstreamDataService, private bundleDataService: BundleDataService, private hostWindowService: HostWindowService, + private configurationDataService: ConfigurationDataService, @Inject(PLATFORM_ID) private platformId: any) { } @@ -163,5 +170,7 @@ export class MiradorViewerComponent implements OnInit { ); } } + // Set the property whether IIIF is enabled in the repository + this.isIiifEnabled$ = this.viewerService.isIiifEnabled(this.configurationDataService); } } diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.service.ts b/src/app/item-page/mirador-viewer/mirador-viewer.service.ts index 0af0f3218e3..9d54c7432c1 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.service.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.service.ts @@ -4,6 +4,7 @@ import { } from '@angular/core'; import { BitstreamDataService } from '@dspace/core/data/bitstream-data.service'; import { BundleDataService } from '@dspace/core/data/bundle-data.service'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; import { PaginatedList } from '@dspace/core/data/paginated-list.model'; import { RemoteData } from '@dspace/core/data/remote-data'; import { Bitstream } from '@dspace/core/shared/bitstream.model'; @@ -14,13 +15,17 @@ import { FollowLinkConfig, } from '@dspace/core/shared/follow-link-config.model'; import { Item } from '@dspace/core/shared/item.model'; -import { getFirstCompletedRemoteData } from '@dspace/core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '@dspace/core/shared/operators'; import { Observable } from 'rxjs'; import { filter, last, map, mergeMap, + startWith, switchMap, } from 'rxjs/operators'; @@ -39,6 +44,22 @@ export class MiradorViewerService { return !isDevMode(); } + /** + * Returns observable of boolean whether IIIF is enabled in the repository. + * The default value is false. + * @param configurationDataService + * @returns the configuration value of iiif.enabled + */ + isIiifEnabled (configurationDataService: ConfigurationDataService): Observable { + return configurationDataService.findByPropertyName('iiif.enabled') + .pipe(getFirstSucceededRemoteDataPayload(), + map((configurationProperty) => + configurationProperty.values?.[0] === 'true', + ), + startWith(false), + ); + } + /** * Returns observable of the number of images found in eligible IIIF bundles. Checks * the mimetype of the first 5 bitstreams in each bundle.