diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 082ffd55a99..0df1e4a43c6 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -420,8 +420,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ); const bundle$ = bitstream$.pipe( - switchMap((bitstream: Bitstream) => bitstream.bundle), - getFirstSucceededRemoteDataPayload(), + switchMap((bitstream: Bitstream) => { + if (hasValue(bitstream) && hasValue(bitstream.bundle)) { + return bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload()); + } + return observableOf(undefined); + }), ); const primaryBitstream$ = bundle$.pipe( @@ -431,12 +435,20 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ); const item$ = bundle$.pipe( - switchMap((bundle: Bundle) => bundle.item), - getFirstSucceededRemoteDataPayload(), + switchMap((bundle: Bundle) => { + if (hasValue(bundle) && hasValue(bundle.item)) { + return bundle.item.pipe(getFirstSucceededRemoteDataPayload()); + } + return observableOf(undefined); + }), ); const format$ = bitstream$.pipe( - switchMap(bitstream => bitstream.format), - getFirstSucceededRemoteDataPayload(), + switchMap((bitstream: Bitstream) => { + if (hasValue(bitstream) && hasValue(bitstream.format)) { + return bitstream.format.pipe(getFirstSucceededRemoteDataPayload()); + } + return observableOf(undefined); + }), ); this.subs.push( @@ -449,15 +461,23 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => { this.bitstream = bitstream as Bitstream; this.bundle = bundle; - this.selectedFormat = format; + if (hasValue(format)) { + this.selectedFormat = format; + } // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will // be a success response, but empty this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; - this.itemId = item.uuid; + if (hasValue(item)) { + this.itemId = item.uuid; + } this.setIiifStatus(this.bitstream); }), format$.pipe(take(1)).subscribe( - (format) => this.originalFormat = format, + (format) => { + if (hasValue(format)) { + this.originalFormat = format; + } + }, ), ); @@ -732,6 +752,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ setIiifStatus(bitstream: Bitstream) { + if (hasValue(bitstream) === false || hasValue(bitstream.bundle) === false || hasValue(bitstream.format) === false) { + this.isIIIF = false; + return; + } + const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/; const regexIIIFItem = /true|yes/i; @@ -745,13 +770,20 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null)); const isEnabled$ = this.bitstream.bundle.pipe( - getFirstSucceededRemoteData(), - map((bundle: RemoteData) => bundle.payload.item.pipe( - getFirstSucceededRemoteData(), - map((item: RemoteData) => - (item.payload.firstMetadataValue('dspace.iiif.enabled') && - item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) - )))); + getFirstSucceededRemoteDataPayload(), + switchMap((bundle: Bundle) => { + if (hasValue(bundle) && hasValue(bundle.item)) { + return bundle.item.pipe( + getFirstSucceededRemoteDataPayload(), + map((item: Item) => { + const iiifEnabledValue = item.firstMetadataValue('dspace.iiif.enabled'); + return hasValue(iiifEnabledValue) && iiifEnabledValue.match(regexIIIFItem) !== null; + }) + ); + } + return observableOf(false); + }) + ); const iiifSub = combineLatest( isImage$, diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts index 333886ed3d1..bb15e2f27d2 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts @@ -62,12 +62,12 @@ export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { getFirstCompletedRemoteData(), getRemoteDataPayload(), switchMap((bitstream: Bitstream) => { - if (hasValue(bitstream)) { + if (hasValue(bitstream) && hasValue(bitstream.bundle)) { return bitstream.bundle.pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), switchMap((bundle: Bundle) => { - if (hasValue(bundle)) { + if (hasValue(bundle) && hasValue(bundle.item)) { return bundle.item.pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts index e85dca2a98d..0cb121c0016 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts @@ -67,6 +67,11 @@ describe('UploadBitstreamComponent', () => { const mockItem = Object.assign(new Item(), { id: 'fake-id', handle: 'fake/handle', + _links: { + self: { + href: '/api/core/items/fake-id' + } + }, metadata: { 'dc.title': [ { @@ -83,6 +88,7 @@ describe('UploadBitstreamComponent', () => { const restEndpoint = 'fake-rest-endpoint'; const mockItemDataService = jasmine.createSpyObj('mockItemDataService', { getBitstreamsEndpoint: observableOf(restEndpoint), + getBundlesEndpoint: observableOf('/api/core/items/fake-id/bundles'), createBundle: createSuccessfulRemoteDataObject$(createdBundle), getBundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [bundle])), }); @@ -97,7 +103,7 @@ describe('UploadBitstreamComponent', () => { const notificationsServiceStub = new NotificationsServiceStub(); const uploaderComponent = jasmine.createSpyObj('uploaderComponent', ['ngOnInit', 'ngAfterViewInit']); const requestService = jasmine.createSpyObj('requestService', { - removeByHrefSubstring: {} + setStaleByHrefSubstring: {} }); describe('when a file is uploaded', () => { @@ -131,6 +137,28 @@ describe('UploadBitstreamComponent', () => { it('should navigate the user to the next page', () => { expect(routerStub.navigate).toHaveBeenCalled(); }); + + it('should clear cached requests for the selected bundle bitstreams endpoint', () => { + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(restEndpoint); + }); + + it('should clear cached requests for the item bundles endpoint', () => { + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('/api/core/items/fake-id/bundles'); + }); + + it('should clear cached requests for the item self endpoint', () => { + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('/api/core/items/fake-id'); + }); + + it('should clear cached requests for the metadatabitstreams byHandle endpoint with encoded handle', () => { + expect(requestService.setStaleByHrefSubstring) + .toHaveBeenCalledWith('/api/core/metadatabitstreams/search/byHandle?handle=fake%2Fhandle&fileGrpType=ORIGINAL'); + }); + + it('should clear cached requests for the metadatabitstreams byHandle endpoint with raw handle', () => { + expect(requestService.setStaleByHrefSubstring) + .toHaveBeenCalledWith('/api/core/metadatabitstreams/search/byHandle?handle=fake/handle&fileGrpType=ORIGINAL'); + }); }); }); diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts index 56b88806eda..1567fbaefab 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts @@ -209,7 +209,28 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { public onCompleteItem(bitstream) { // Clear cached requests for this bundle's bitstreams to ensure lists on all pages are up-to-date this.bundleService.getBitstreamsEndpoint(this.selectedBundleId).pipe(take(1)).subscribe((href: string) => { - this.requestService.removeByHrefSubstring(href); + this.requestService.setStaleByHrefSubstring(href); + }); + + // Clear cached requests for this item's bundles to ensure bundle resolution uses fresh data + this.itemService.getBundlesEndpoint(this.itemId).pipe(take(1)).subscribe((href: string) => { + this.requestService.setStaleByHrefSubstring(href); + }); + + // Clear cached requests for this item to ensure breadcrumb navigation resolves a fresh item + this.itemRD$.pipe( + getFirstSucceededRemoteDataPayload(), + take(1), + ).subscribe((item: Item) => { + this.requestService.setStaleByHrefSubstring(item._links.self.href); + + // Clear metadatabitstreams search cache used by preview and CLARIN files sections + if (item?.handle) { + const byHandleBase = '/api/core/metadatabitstreams/search/byHandle'; + const encodedHandle = encodeURIComponent(item.handle); + this.requestService.setStaleByHrefSubstring(`${byHandleBase}?handle=${encodedHandle}&fileGrpType=ORIGINAL`); + this.requestService.setStaleByHrefSubstring(`${byHandleBase}?handle=${item.handle}&fileGrpType=ORIGINAL`); + } }); // Bring over the item ID as a query parameter diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 879c4b889c5..686190adad6 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { Item } from '../../core/shared/item.model'; import { getAllSucceededRemoteListPayload, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getItemPageRoute } from '../item-page-routing-paths'; @@ -7,7 +7,7 @@ import { RegistryService } from '../../core/registry/registry.service'; import { Router } from '@angular/router'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ @@ -15,7 +15,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './clarin-files-section.component.html', styleUrls: ['./clarin-files-section.component.scss'] }) -export class ClarinFilesSectionComponent implements OnInit { +export class ClarinFilesSectionComponent implements OnInit, OnChanges, OnDestroy { /** * The item to display files for @@ -72,6 +72,9 @@ export class ClarinFilesSectionComponent implements OnInit { */ downloadZipMinFileCount: BehaviorSubject = new BehaviorSubject(-1); + private currentItemHandle: string; + private filesSubscription?: Subscription; + constructor(protected registryService: RegistryService, protected router: Router, @@ -81,17 +84,19 @@ export class ClarinFilesSectionComponent implements OnInit { } ngOnInit(): void { - this.registryService - .getMetadataBitstream(this.itemHandle, 'ORIGINAL') - .pipe(getAllSucceededRemoteListPayload()) - .subscribe((data: MetadataBitstream[]) => { - this.listOfFiles.next(data); - this.generateCurlCommand(); - }); - this.totalFileSizes.next(Number(this.item.firstMetadataValue('local.files.size'))); this.loadDownloadZipConfigProperties(); } + ngOnChanges(changes: SimpleChanges): void { + if (changes.item || changes.itemHandle) { + this.refreshFromInputs(true); + } + } + + ngOnDestroy(): void { + this.filesSubscription?.unsubscribe(); + } + openCommandModal(content: any) { this.commandCopied = false; this.modalService.open(content, { size: 'lg', centered: true, ariaLabelledBy: 'commandModalTitle' }); @@ -161,4 +166,29 @@ export class ClarinFilesSectionComponent implements OnInit { this.downloadZipMinFileSize.next(Number(config.values[0])); }); } + + private refreshFromInputs(force = false): void { + if (this.item) { + this.totalFileSizes.next(Number(this.item.firstMetadataValue('local.files.size'))); + } + + const handle = this.itemHandle || this.item?.handle; + if (!handle) { + return; + } + + if (!force && handle === this.currentItemHandle) { + return; + } + + this.currentItemHandle = handle; + this.filesSubscription?.unsubscribe(); + this.filesSubscription = this.registryService + .getMetadataBitstream(handle, 'ORIGINAL') + .pipe(getAllSucceededRemoteListPayload()) + .subscribe((data: MetadataBitstream[]) => { + this.listOfFiles.next(data); + this.generateCurlCommand(); + }); + } } diff --git a/src/app/item-page/item.resolver.ts b/src/app/item-page/item.resolver.ts index ffeef57ecb0..c5225675991 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -52,7 +52,7 @@ export class ItemResolver implements Resolve> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const itemRD$ = this.itemService.findById(route.params.id, true, - false, + true, ...getItemPageLinksToFollow(), ).pipe( getFirstCompletedRemoteData(), diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts index 1bac02083e7..f2b5341ec01 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BehaviorSubject, of } from 'rxjs'; import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; import { RegistryService } from 'src/app/core/registry/registry.service'; +import { SimpleChange } from '@angular/core'; import { PreviewSectionComponent } from './preview-section.component'; import { ResourceType } from 'src/app/core/shared/resource-type'; @@ -75,7 +76,10 @@ describe('PreviewSectionComponent', () => { expect(component).toBeTruthy(); }); - it('should call getMetadataBitstream on init', () => { + it('should call getMetadataBitstream on item input change', () => { + component.ngOnChanges({ + item: new SimpleChange(undefined, component.item, true), + }); expect(mockRegistryService.getMetadataBitstream).toHaveBeenCalled(); }); diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts index d8365511dbd..73e858da35a 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; import { RegistryService } from 'src/app/core/registry/registry.service'; import { Item } from 'src/app/core/shared/item.model'; @@ -11,26 +11,53 @@ import { ConfigurationDataService } from '../../../../core/data/configuration-da templateUrl: './preview-section.component.html', styleUrls: ['./preview-section.component.scss'], }) -export class PreviewSectionComponent implements OnInit { +export class PreviewSectionComponent implements OnInit, OnChanges, OnDestroy { @Input() item: Item; listOfFiles: BehaviorSubject = new BehaviorSubject([] as any); emailToContact: string; hasNoFiles: BehaviorSubject = new BehaviorSubject(true); + private currentItemHandle: string; + private filesSubscription?: Subscription; + constructor(protected registryService: RegistryService, - private configService: ConfigurationDataService) {} // Modified + private configService: ConfigurationDataService) {} ngOnInit(): void { - this.registryService - .getMetadataBitstream(this.item.handle, 'ORIGINAL') + this.configService.findByPropertyName('lr.help.mail')?.subscribe(remoteData => { + this.emailToContact = remoteData.payload?.values?.[0]; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.item) { + this.refreshFiles(true); + } + } + + ngOnDestroy(): void { + this.filesSubscription?.unsubscribe(); + } + + private refreshFiles(force = false): void { + const handle = this.item?.handle; + if (!handle) { + return; + } + + if (!force && handle === this.currentItemHandle) { + return; + } + + this.currentItemHandle = handle; + this.filesSubscription?.unsubscribe(); + this.filesSubscription = this.registryService + .getMetadataBitstream(handle, 'ORIGINAL') .pipe(getAllSucceededRemoteListPayload()) .subscribe((data: MetadataBitstream[]) => { this.listOfFiles.next(data); this.hasNoFiles.next(!Array.isArray(data) || data.length === 0); }); - this.configService.findByPropertyName('lr.help.mail')?.subscribe(remoteData => { - this.emailToContact = remoteData.payload?.values?.[0]; - }); } }